diff options
Diffstat (limited to 'browser/components/preferences')
209 files changed, 46627 insertions, 0 deletions
diff --git a/browser/components/preferences/containers.inc.xhtml b/browser/components/preferences/containers.inc.xhtml new file mode 100644 index 0000000000..7e2a7172dd --- /dev/null +++ b/browser/components/preferences/containers.inc.xhtml @@ -0,0 +1,42 @@ +# 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/. + +<!-- Containers panel --> + +<script src="chrome://browser/content/preferences/containers.js"/> + +<hbox hidden="true" + class="container-header-links" + data-category="paneContainers"> + <button id="backContainersButton" class="back-button" data-l10n-id="containers-back-button2"/> +</hbox> + +<hbox id="header-containers" + class="header" + hidden="true" + data-category="paneContainers"> + <html:h1 data-l10n-id="containers-header"/> +</hbox> + +<!-- Containers --> +<groupbox id="browserContainersGroupPane" data-category="paneContainers" hidden="true" + data-hidden-from-search="true" data-subpanel="true"> + <vbox id="browserContainersbox"> + <richlistbox id="containersView"/> + </vbox> + <vbox> + <hbox flex="1"> + <button id="containersAdd" + is="highlightable-button" + data-l10n-id="containers-add-button"/> + </hbox> + </vbox> + <vbox> + <hbox flex="1"> + <checkbox id="containersNewTabCheck" + data-l10n-id="containers-new-tab-check" + preference="privacy.userContext.newTabContainerOnLeftClick.enabled"/> + </hbox> + </vbox> +</groupbox> diff --git a/browser/components/preferences/containers.js b/browser/components/preferences/containers.js new file mode 100644 index 0000000000..80d1ec7cc3 --- /dev/null +++ b/browser/components/preferences/containers.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from preferences.js */ + +const defaultContainerIcon = "fingerprint"; +const defaultContainerColor = "blue"; + +let gContainersPane = { + init() { + this._list = document.getElementById("containersView"); + + document + .getElementById("backContainersButton") + .addEventListener("command", function () { + gotoPref("general"); + }); + + document + .getElementById("containersAdd") + .addEventListener("command", function () { + gContainersPane.onAddButtonCommand(); + }); + + this._rebuildView(); + }, + + _rebuildView() { + const containers = ContextualIdentityService.getPublicIdentities(); + while (this._list.firstChild) { + this._list.firstChild.remove(); + } + for (let container of containers) { + let item = document.createXULElement("richlistitem"); + + let outer = document.createXULElement("hbox"); + outer.setAttribute("flex", 1); + outer.setAttribute("align", "center"); + item.appendChild(outer); + + let userContextIcon = document.createXULElement("hbox"); + userContextIcon.className = "userContext-icon"; + userContextIcon.classList.add("userContext-icon-inprefs"); + userContextIcon.classList.add("identity-icon-" + container.icon); + userContextIcon.classList.add("identity-color-" + container.color); + outer.appendChild(userContextIcon); + + let label = document.createXULElement("label"); + label.className = "userContext-label-inprefs"; + label.setAttribute("flex", 1); + let containerName = ContextualIdentityService.getUserContextLabel( + container.userContextId + ); + label.textContent = containerName; + label.setAttribute("tooltiptext", containerName); + outer.appendChild(label); + + let containerButtons = document.createXULElement("hbox"); + containerButtons.className = "container-buttons"; + item.appendChild(containerButtons); + + let prefsButton = document.createXULElement("button"); + prefsButton.addEventListener("command", function (event) { + gContainersPane.onPreferenceCommand(event.originalTarget); + }); + prefsButton.setAttribute("value", container.userContextId); + document.l10n.setAttributes(prefsButton, "containers-settings-button"); + containerButtons.appendChild(prefsButton); + + let removeButton = document.createXULElement("button"); + removeButton.addEventListener("command", function (event) { + gContainersPane.onRemoveCommand(event.originalTarget); + }); + removeButton.setAttribute("value", container.userContextId); + document.l10n.setAttributes(removeButton, "containers-remove-button"); + containerButtons.appendChild(removeButton); + + this._list.appendChild(item); + } + }, + + async onRemoveCommand(button) { + let userContextId = parseInt(button.getAttribute("value"), 10); + + let count = ContextualIdentityService.countContainerTabs(userContextId); + if (count > 0) { + let [title, message, okButton, cancelButton] = + await document.l10n.formatValues([ + { id: "containers-remove-alert-title" }, + { id: "containers-remove-alert-msg", args: { count } }, + { id: "containers-remove-ok-button" }, + { id: "containers-remove-cancel-button" }, + ]); + + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1; + + let rv = Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + okButton, + cancelButton, + null, + null, + {} + ); + if (rv != 0) { + return; + } + + await ContextualIdentityService.closeContainerTabs(userContextId); + } + + ContextualIdentityService.remove(userContextId); + this._rebuildView(); + }, + + onPreferenceCommand(button) { + this.openPreferenceDialog(button.getAttribute("value")); + }, + + onAddButtonCommand(button) { + this.openPreferenceDialog(null); + }, + + openPreferenceDialog(userContextId) { + let identity = { + name: "", + icon: defaultContainerIcon, + color: defaultContainerColor, + }; + if (userContextId) { + identity = + ContextualIdentityService.getPublicIdentityFromId(userContextId); + identity.name = ContextualIdentityService.getUserContextLabel( + identity.userContextId + ); + } + + const params = { userContextId, identity }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/containers.xhtml", + undefined, + params + ); + }, +}; diff --git a/browser/components/preferences/dialogs/addEngine.css b/browser/components/preferences/dialogs/addEngine.css new file mode 100644 index 0000000000..450e07f65f --- /dev/null +++ b/browser/components/preferences/dialogs/addEngine.css @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +input { + flex: 1; +} + +hbox { + width: 100%; +} + +#engineNameLabel, +#engineUrlLabel, +#engineAliasLabel { + /* Align the labels with the inputs */ + margin-inline-start: 4px; +} + +#engineUrl { + /* Full URLs should always be displayed as LTR */ + direction: ltr; + text-align: match-parent; +} diff --git a/browser/components/preferences/dialogs/addEngine.js b/browser/components/preferences/dialogs/addEngine.js new file mode 100644 index 0000000000..1faf8622b3 --- /dev/null +++ b/browser/components/preferences/dialogs/addEngine.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../main.js */ + +let gAddEngineDialog = { + _form: null, + _name: null, + _alias: null, + + onLoad() { + document.mozSubdialogReady = this.init(); + }, + + async init() { + this._dialog = document.querySelector("dialog"); + this._form = document.getElementById("addEngineForm"); + this._name = document.getElementById("engineName"); + this._alias = document.getElementById("engineAlias"); + + this._name.addEventListener("input", this.onNameInput.bind(this)); + this._alias.addEventListener("input", this.onAliasInput.bind(this)); + this._form.addEventListener("input", this.onFormInput.bind(this)); + + document.addEventListener("dialogaccept", this.onAddEngine.bind(this)); + }, + + async onAddEngine(event) { + let url = document + .getElementById("engineUrl") + .value.replace(/%s/, "{searchTerms}"); + await Services.search.wrappedJSObject.addUserEngine( + this._name.value, + url, + this._alias.value + ); + }, + + async onNameInput() { + if (this._name.value) { + let engine = Services.search.getEngineByName(this._name.value); + let validity = engine + ? document.getElementById("engineNameExists").textContent + : ""; + this._name.setCustomValidity(validity); + } + }, + + async onAliasInput() { + let validity = ""; + if (this._alias.value) { + let engine = await Services.search.getEngineByAlias(this._alias.value); + if (engine) { + engine = document.getElementById("engineAliasExists").textContent; + } + } + this._alias.setCustomValidity(validity); + }, + + async onFormInput() { + this._dialog.setAttribute( + "buttondisabledaccept", + !this._form.checkValidity() + ); + }, +}; + +window.addEventListener("load", () => gAddEngineDialog.onLoad()); diff --git a/browser/components/preferences/dialogs/addEngine.xhtml b/browser/components/preferences/dialogs/addEngine.xhtml new file mode 100644 index 0000000000..780cda9010 --- /dev/null +++ b/browser/components/preferences/dialogs/addEngine.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/addEngine.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="add-engine-window2" + data-l10n-attrs="title, style" + persist="width height" +> + <dialog + buttons="accept,cancel" + buttondisabledaccept="true" + data-l10n-id="add-engine-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="browser/preferences/addEngine.ftl" /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/addEngine.js" /> + <script src="chrome://global/content/globalOverlay.js" /> + <script src="chrome://browser/content/utilityOverlay.js" /> + + <separator class="thin" /> + + <html:form id="addEngineForm"> + <html:span + id="engineNameExists" + hidden="hidden" + data-l10n-id="engine-name-exists" + /> + <html:label + id="engineNameLabel" + for="engineName" + data-l10n-id="add-engine-name" + /> + <hbox> + <html:input id="engineName" type="text" required="required" /> + </hbox> + + <html:label + id="engineUrlLabel" + for="engineUrl" + data-l10n-id="add-engine-url" + /> + <hbox> + <html:input id="engineUrl" type="url" required="required" /> + </hbox> + + <html:span + id="engineAliasExists" + hidden="hidden" + data-l10n-id="engine-alias-exists" + /> + <html:label + id="engineAliasLabel" + for="engineAlias" + data-l10n-id="add-engine-alias" + /> + <hbox> + <html:input id="engineAlias" type="text" /> + </hbox> + </html:form> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/applicationManager.js b/browser/components/preferences/dialogs/applicationManager.js new file mode 100644 index 0000000000..0c2105cbe4 --- /dev/null +++ b/browser/components/preferences/dialogs/applicationManager.js @@ -0,0 +1,129 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/* import-globals-from ../main.js */ + +var gAppManagerDialog = { + _removed: [], + + onLoad() { + document.mozSubdialogReady = this.init(); + }, + + async init() { + this.handlerInfo = window.arguments[0]; + + document.addEventListener("dialogaccept", function () { + gAppManagerDialog.onOK(); + }); + + let gMainPane = window.parent.gMainPane; + + const appDescElem = document.getElementById("appDescription"); + if (this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) { + let { typeDescription } = this.handlerInfo; + let typeStr; + if (typeDescription.id) { + MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl"); + typeStr = await document.l10n.formatValue( + typeDescription.id, + typeDescription.args + ); + } else { + typeStr = typeDescription.raw; + } + document.l10n.setAttributes(appDescElem, "app-manager-handle-file", { + type: typeStr, + }); + } else { + document.l10n.setAttributes(appDescElem, "app-manager-handle-protocol", { + type: this.handlerInfo.typeDescription.raw, + }); + } + + let list = document.getElementById("appList"); + let listFragment = document.createDocumentFragment(); + for (let app of this.handlerInfo.possibleApplicationHandlers.enumerate()) { + if (!gMainPane.isValidHandlerApp(app)) { + continue; + } + + let item = document.createXULElement("richlistitem"); + listFragment.append(item); + item.app = app; + + let image = document.createXULElement("image"); + image.setAttribute("src", gMainPane._getIconURLForHandlerApp(app)); + item.appendChild(image); + + let label = document.createXULElement("label"); + label.setAttribute("value", app.name); + item.appendChild(label); + } + list.append(listFragment); + + // Triggers onSelect which populates label + list.selectedIndex = 0; + + // We want to block on those elements being localized because the + // result will impact the size of the subdialog. + await document.l10n.translateElements([ + appDescElem, + document.getElementById("appType"), + ]); + }, + + onOK: function appManager_onOK() { + if (this._removed.length) { + for (var i = 0; i < this._removed.length; ++i) { + this.handlerInfo.removePossibleApplicationHandler(this._removed[i]); + } + + this.handlerInfo.store(); + } + }, + + remove: function appManager_remove() { + var list = document.getElementById("appList"); + this._removed.push(list.selectedItem.app); + var index = list.selectedIndex; + var element = list.selectedItem; + list.removeItemFromSelection(element); + element.remove(); + if (list.itemCount == 0) { + // The list is now empty, make the bottom part disappear + document.getElementById("appDetails").hidden = true; + } else { + // Select the item at the same index, if we removed the last + // item of the list, select the previous item + if (index == list.itemCount) { + --index; + } + list.selectedIndex = index; + } + }, + + onSelect: function appManager_onSelect() { + var list = document.getElementById("appList"); + if (!list.selectedItem) { + document.getElementById("remove").disabled = true; + return; + } + document.getElementById("remove").disabled = false; + var app = list.selectedItem.app; + var address = ""; + if (app instanceof Ci.nsILocalHandlerApp) { + address = app.executable.path; + } else if (app instanceof Ci.nsIWebHandlerApp) { + address = app.uriTemplate; + } + document.getElementById("appLocation").value = address; + const l10nId = + app instanceof Ci.nsILocalHandlerApp + ? "app-manager-local-app-info" + : "app-manager-web-app-info"; + const appTypeElem = document.getElementById("appType"); + document.l10n.setAttributes(appTypeElem, l10nId); + }, +}; diff --git a/browser/components/preferences/dialogs/applicationManager.xhtml b/browser/components/preferences/dialogs/applicationManager.xhtml new file mode 100644 index 0000000000..889307d88c --- /dev/null +++ b/browser/components/preferences/dialogs/applicationManager.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gAppManagerDialog.onLoad();" + data-l10n-id="app-manager-window2" + data-l10n-attrs="title, style" +> + <dialog id="appManager" buttons="accept,cancel"> + <linkset> + <html:link + rel="localization" + href="browser/preferences/applicationManager.ftl" + /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://browser/content/preferences/dialogs/applicationManager.js" /> + + <commandset id="appManagerCommandSet"> + <command + id="cmd_remove" + oncommand="gAppManagerDialog.remove();" + disabled="true" + /> + </commandset> + + <keyset id="appManagerKeyset"> + <key id="delete" keycode="VK_DELETE" command="cmd_remove" /> + </keyset> + + <description id="appDescription" /> + <separator class="thin" /> + <hbox flex="1"> + <richlistbox + id="appList" + onselect="gAppManagerDialog.onSelect();" + flex="1" + /> + <vbox> + <button + id="remove" + data-l10n-id="app-manager-remove" + command="cmd_remove" + /> + <spacer flex="1" /> + </vbox> + </hbox> + <vbox id="appDetails"> + <separator class="thin" /> + <label id="appType" /> + <html:input + type="text" + id="appLocation" + readonly="readonly" + style="margin-inline: 0" + /> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/blocklists.js b/browser/components/preferences/dialogs/blocklists.js new file mode 100644 index 0000000000..c28ee09f96 --- /dev/null +++ b/browser/components/preferences/dialogs/blocklists.js @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const BASE_LIST_ID = "base"; +const CONTENT_LIST_ID = "content"; +const TRACK_SUFFIX = "-track-digest256"; +const TRACKING_TABLE_PREF = "urlclassifier.trackingTable"; +const LISTS_PREF_BRANCH = "browser.safebrowsing.provider.mozilla.lists."; + +var gBlocklistManager = { + _type: "", + _blockLists: [], + _tree: null, + + _view: { + _rowCount: 0, + get rowCount() { + return this._rowCount; + }, + getCellText(row, column) { + if (column.id == "listCol") { + let list = gBlocklistManager._blockLists[row]; + return list.name; + } + return ""; + }, + + isSeparator(index) { + return false; + }, + isSorted() { + return false; + }, + isContainer(index) { + return false; + }, + setTree(tree) {}, + getImageSrc(row, column) {}, + getCellValue(row, column) { + if (column.id == "selectionCol") { + return gBlocklistManager._blockLists[row].selected; + } + return undefined; + }, + cycleHeader(column) {}, + getRowProperties(row) { + return ""; + }, + getColumnProperties(column) { + return ""; + }, + getCellProperties(row, column) { + if (column.id == "selectionCol") { + return "checkmark"; + } + + return ""; + }, + }, + + onLoad() { + this.init(); + document.addEventListener("dialogaccept", () => this.onApplyChanges()); + }, + + init() { + if (this._type) { + // reusing an open dialog, clear the old observer + this.uninit(); + } + + this._type = "tracking"; + + this._loadBlockLists(); + }, + + uninit() {}, + + onListSelected() { + for (let list of this._blockLists) { + list.selected = false; + } + this._blockLists[this._tree.currentIndex].selected = true; + + this._updateTree(); + }, + + onApplyChanges() { + let activeList = this._getActiveList(); + let selected = null; + for (let list of this._blockLists) { + if (list.selected) { + selected = list; + break; + } + } + + if (activeList !== selected.id) { + let trackingTable = Services.prefs.getCharPref(TRACKING_TABLE_PREF); + if (selected.id != CONTENT_LIST_ID) { + trackingTable = trackingTable.replace( + "," + CONTENT_LIST_ID + TRACK_SUFFIX, + "" + ); + } else { + trackingTable += "," + CONTENT_LIST_ID + TRACK_SUFFIX; + } + Services.prefs.setCharPref(TRACKING_TABLE_PREF, trackingTable); + + // Force an update after changing the tracking protection table. + let listmanager = Cc[ + "@mozilla.org/url-classifier/listmanager;1" + ].getService(Ci.nsIUrlListManager); + if (listmanager) { + listmanager.forceUpdates(trackingTable); + } + } + }, + + async _loadBlockLists() { + this._blockLists = []; + + // Load blocklists into a table. + let branch = Services.prefs.getBranch(LISTS_PREF_BRANCH); + let itemArray = branch.getChildList(""); + for (let itemName of itemArray) { + try { + let list = await this._createBlockList(itemName); + this._blockLists.push(list); + } catch (e) { + // Ignore bogus or missing list name. + continue; + } + } + + this._updateTree(); + }, + + async _createBlockList(id) { + let branch = Services.prefs.getBranch(LISTS_PREF_BRANCH); + let l10nKey = branch.getCharPref(id); + + // eslint-disable-next-line mozilla/prefer-formatValues + let [listName, description] = await document.l10n.formatValues([ + { id: `blocklist-item-${l10nKey}-listName` }, + { id: `blocklist-item-${l10nKey}-description` }, + ]); + + // eslint-disable-next-line mozilla/prefer-formatValues + let name = await document.l10n.formatValue("blocklist-item-list-template", { + listName, + description, + }); + + return { + id, + name, + selected: this._getActiveList() === id, + }; + }, + + _updateTree() { + this._tree = document.getElementById("blocklistsTree"); + this._view._rowCount = this._blockLists.length; + this._tree.view = this._view; + }, + + _getActiveList() { + let trackingTable = Services.prefs.getCharPref(TRACKING_TABLE_PREF); + return trackingTable.includes(CONTENT_LIST_ID) + ? CONTENT_LIST_ID + : BASE_LIST_ID; + }, +}; diff --git a/browser/components/preferences/dialogs/blocklists.xhtml b/browser/components/preferences/dialogs/blocklists.xhtml new file mode 100644 index 0000000000..eda42ee7ae --- /dev/null +++ b/browser/components/preferences/dialogs/blocklists.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window + id="BlocklistsDialog" + data-l10n-id="blocklist-window2" + data-l10n-attrs="title, style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gBlocklistManager.onLoad();" + onunload="gBlocklistManager.uninit();" + persist="width height" +> + <dialog + buttons="accept,cancel" + data-l10n-id="blocklist-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link rel="localization" href="browser/preferences/blocklists.ftl" /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/blocklists.js" /> + + <keyset> + <key + data-l10n-id="blocklist-close-key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane"> + <description + id="blocklistsText" + data-l10n-id="blocklist-description" + control="url" + > + <html:a + target="_blank" + class="text-link" + data-l10n-name="disconnect-link" + href="https://disconnect.me/" + /> + </description> + <separator class="thin" /> + <tree + id="blocklistsTree" + flex="1" + style="height: 18em" + hidecolumnpicker="true" + onselect="gBlocklistManager.onListSelected();" + > + <treecols> + <treecol + id="selectionCol" + label="" + style="flex: 1 auto" + sortable="false" + type="checkbox" + /> + <treecol + id="listCol" + data-l10n-id="blocklist-treehead-list" + style="flex: 80 80 auto" + sortable="false" + /> + </treecols> + <treechildren /> + </tree> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/browserLanguages.js b/browser/components/preferences/dialogs/browserLanguages.js new file mode 100644 index 0000000000..3dc7e3f9ff --- /dev/null +++ b/browser/components/preferences/dialogs/browserLanguages.js @@ -0,0 +1,731 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +// This is exported by preferences.js but we can't import that in a subdialog. +let { LangPackMatcher } = window.top; + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); +ChromeUtils.defineModuleGetter( + this, + "SelectionChangedMenulist", + "resource:///modules/SelectionChangedMenulist.jsm" +); + +document + .getElementById("BrowserLanguagesDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); + +/* This dialog provides an interface for managing what language the browser is + * displayed in. + * + * There is a list of "requested" locales and a list of "available" locales. The + * requested locales must be installed and enabled. Available locales could be + * installed and enabled, or fetched from the AMO language tools API. + * + * If a langpack is disabled, there is no way to determine what locale it is for and + * it will only be listed as available if that locale is also available on AMO and + * the user has opted to search for more languages. + */ + +async function installFromUrl(url, hash, callback) { + let telemetryInfo = { + source: "about:preferences", + }; + let install = await AddonManager.getInstallForURL(url, { + hash, + telemetryInfo, + }); + if (callback) { + callback(install.installId.toString()); + } + await install.install(); + return install.addon; +} + +async function dictionaryIdsForLocale(locale) { + let entries = await RemoteSettings("language-dictionaries").get({ + filters: { id: locale }, + }); + if (entries.length) { + return entries[0].dictionaries; + } + return []; +} + +class OrderedListBox { + constructor({ + richlistbox, + upButton, + downButton, + removeButton, + onRemove, + onReorder, + }) { + this.richlistbox = richlistbox; + this.upButton = upButton; + this.downButton = downButton; + this.removeButton = removeButton; + this.onRemove = onRemove; + this.onReorder = onReorder; + + this.items = []; + + this.richlistbox.addEventListener("select", () => this.setButtonState()); + this.upButton.addEventListener("command", () => this.moveUp()); + this.downButton.addEventListener("command", () => this.moveDown()); + this.removeButton.addEventListener("command", () => this.removeItem()); + } + + get selectedItem() { + return this.items[this.richlistbox.selectedIndex]; + } + + setButtonState() { + let { upButton, downButton, removeButton } = this; + let { selectedIndex, itemCount } = this.richlistbox; + upButton.disabled = selectedIndex <= 0; + downButton.disabled = selectedIndex == itemCount - 1; + removeButton.disabled = itemCount <= 1 || !this.selectedItem.canRemove; + } + + moveUp() { + let { selectedIndex } = this.richlistbox; + if (selectedIndex == 0) { + return; + } + let { items } = this; + let selectedItem = items[selectedIndex]; + let prevItem = items[selectedIndex - 1]; + items[selectedIndex - 1] = items[selectedIndex]; + items[selectedIndex] = prevItem; + let prevEl = document.getElementById(prevItem.id); + let selectedEl = document.getElementById(selectedItem.id); + this.richlistbox.insertBefore(selectedEl, prevEl); + this.richlistbox.ensureElementIsVisible(selectedEl); + this.setButtonState(); + + this.onReorder(); + } + + moveDown() { + let { selectedIndex } = this.richlistbox; + if (selectedIndex == this.items.length - 1) { + return; + } + let { items } = this; + let selectedItem = items[selectedIndex]; + let nextItem = items[selectedIndex + 1]; + items[selectedIndex + 1] = items[selectedIndex]; + items[selectedIndex] = nextItem; + let nextEl = document.getElementById(nextItem.id); + let selectedEl = document.getElementById(selectedItem.id); + this.richlistbox.insertBefore(nextEl, selectedEl); + this.richlistbox.ensureElementIsVisible(selectedEl); + this.setButtonState(); + + this.onReorder(); + } + + removeItem() { + let { selectedIndex } = this.richlistbox; + + if (selectedIndex == -1) { + return; + } + + let [item] = this.items.splice(selectedIndex, 1); + this.richlistbox.selectedItem.remove(); + this.richlistbox.selectedIndex = Math.min( + selectedIndex, + this.richlistbox.itemCount - 1 + ); + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + this.onRemove(item); + } + + setItems(items) { + this.items = items; + this.populate(); + this.setButtonState(); + } + + /** + * Add an item to the top of the ordered list. + * + * @param {object} item The item to insert. + */ + addItem(item) { + this.items.unshift(item); + this.richlistbox.insertBefore( + this.createItem(item), + this.richlistbox.firstElementChild + ); + this.richlistbox.selectedIndex = 0; + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + } + + populate() { + this.richlistbox.textContent = ""; + + let frag = document.createDocumentFragment(); + for (let item of this.items) { + frag.appendChild(this.createItem(item)); + } + this.richlistbox.appendChild(frag); + + this.richlistbox.selectedIndex = 0; + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + } + + createItem({ id, label, value }) { + let listitem = document.createXULElement("richlistitem"); + listitem.id = id; + listitem.setAttribute("value", value); + + let labelEl = document.createXULElement("label"); + labelEl.textContent = label; + listitem.appendChild(labelEl); + + return listitem; + } +} + +/** + * The sorted select list of Locales available for the app. + */ +class SortedItemSelectList { + constructor({ menulist, button, onSelect, onChange, compareFn }) { + /** @type {XULElement} */ + this.menulist = menulist; + + /** @type {XULElement} */ + this.popup = menulist.menupopup; + + /** @type {XULElement} */ + this.button = button; + + /** @type {(a: LocaleDisplayInfo, b: LocaleDisplayInfo) => number} */ + this.compareFn = compareFn; + + /** @type {Array<LocaleDisplayInfo>} */ + this.items = []; + + // This will register the "command" listener. + new SelectionChangedMenulist(this.menulist, () => { + button.disabled = !menulist.selectedItem; + if (menulist.selectedItem) { + onChange(this.items[menulist.selectedIndex]); + } + }); + button.addEventListener("command", () => { + if (!menulist.selectedItem) { + return; + } + + let [item] = this.items.splice(menulist.selectedIndex, 1); + menulist.selectedItem.remove(); + menulist.setAttribute("label", menulist.getAttribute("placeholder")); + button.disabled = true; + menulist.disabled = menulist.itemCount == 0; + menulist.selectedIndex = -1; + + onSelect(item); + }); + } + + /** + * @param {Array<LocaleDisplayInfo>} items + */ + setItems(items) { + this.items = items.sort(this.compareFn); + this.populate(); + } + + populate() { + let { button, items, menulist, popup } = this; + popup.textContent = ""; + + let frag = document.createDocumentFragment(); + for (let item of items) { + frag.appendChild(this.createItem(item)); + } + popup.appendChild(frag); + + menulist.setAttribute("label", menulist.getAttribute("placeholder")); + menulist.disabled = menulist.itemCount == 0; + menulist.selectedIndex = -1; + button.disabled = true; + } + + /** + * Add an item to the list sorted by the label. + * + * @param {object} item The item to insert. + */ + addItem(item) { + let { compareFn, items, menulist, popup } = this; + + // Find the index of the item to insert before. + let i = items.findIndex(el => compareFn(el, item) >= 0); + items.splice(i, 0, item); + popup.insertBefore(this.createItem(item), menulist.getItemAtIndex(i)); + + menulist.disabled = menulist.itemCount == 0; + } + + createItem({ label, value, className, disabled }) { + let item = document.createXULElement("menuitem"); + item.setAttribute("label", label); + if (value) { + item.value = value; + } + if (className) { + item.classList.add(className); + } + if (disabled) { + item.setAttribute("disabled", "true"); + } + return item; + } + + /** + * Disable the inputs and set a data-l10n-id on the menulist. This can be + * reverted with `enableWithMessageId()`. + */ + disableWithMessageId(messageId) { + this.menulist.setAttribute("data-l10n-id", messageId); + this.menulist.setAttribute( + "image", + "chrome://browser/skin/tabbrowser/tab-connecting.png" + ); + this.menulist.disabled = true; + this.button.disabled = true; + } + + /** + * Enable the inputs and set a data-l10n-id on the menulist. This can be + * reverted with `disableWithMessageId()`. + */ + enableWithMessageId(messageId) { + this.menulist.setAttribute("data-l10n-id", messageId); + this.menulist.removeAttribute("image"); + this.menulist.disabled = this.menulist.itemCount == 0; + this.button.disabled = !this.menulist.selectedItem; + } +} + +/** + * @typedef LocaleDisplayInfo + * @type {object} + * @prop {string} id - A unique ID. + * @prop {string} label - The localized display name. + * @prop {string} value - The BCP 47 locale identifier or the word "search". + * @prop {boolean} canRemove - Locales that are part of the packaged locales cannot be + * removed. + * @prop {boolean} installed - Whether or not the locale is installed. + */ + +/** + * @param {Array<string>} localeCodes - List of BCP 47 locale identifiers. + * @returns {Array<LocaleDisplayInfo>} + */ +async function getLocaleDisplayInfo(localeCodes) { + let availableLocales = new Set(await LangPackMatcher.getAvailableLocales()); + let packagedLocales = new Set(Services.locale.packagedLocales); + let localeNames = Services.intl.getLocaleDisplayNames( + undefined, + localeCodes, + { preferNative: true } + ); + return localeCodes.map((code, i) => { + return { + id: "locale-" + code, + label: localeNames[i], + value: code, + canRemove: !packagedLocales.has(code), + installed: availableLocales.has(code), + }; + }); +} + +/** + * @param {LocaleDisplayInfo} a + * @param {LocaleDisplayInfo} b + * @returns {number} + */ +function compareItems(a, b) { + // Sort by installed. + if (a.installed != b.installed) { + return a.installed ? -1 : 1; + + // The search label is always last. + } else if (a.value == "search") { + return 1; + } else if (b.value == "search") { + return -1; + + // If both items are locales, sort by label. + } else if (a.value && b.value) { + return a.label.localeCompare(b.label); + + // One of them is a label, put it first. + } else if (a.value) { + return 1; + } + return -1; +} + +var gBrowserLanguagesDialog = { + /** + * The publicly readable list of selected locales. It is only set when the dialog is + * accepted, and can be retrieved elsewhere by directly reading the property + * on gBrowserLanguagesDialog. + * + * let { selected } = gBrowserLanguagesDialog; + * + * @type {null | Array<string>} + */ + selected: null, + + /** + * @type {string | null} An ID used for telemetry pings. It is unique to the current + * opening of the browser language. + */ + _telemetryId: null, + + /** + * @type {SortedItemSelectList} + */ + _availableLocalesUI: null, + + /** + * @type {OrderedListBox} + */ + _selectedLocalesUI: null, + + get downloadEnabled() { + // Downloading langpacks isn't always supported, check the pref. + return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled"); + }, + + recordTelemetry(method, extra = null) { + Services.telemetry.recordEvent( + "intl.ui.browserLanguage", + method, + "dialog", + this._telemetryId, + extra + ); + }, + + async onLoad() { + /** + * @typedef {Object} Options - Options passed in to configure the subdialog. + * @property {string} telemetryId, + * @property {Array<string>} [selectedLocalesForRestart] The optional list of + * previously selected locales for when a restart is required. This list is + * preserved between openings of the dialog. + * @property {boolean} search Whether the user opened this from "Search for more + * languages" option. + */ + + /** @type {Options} */ + let { telemetryId, selectedLocalesForRestart, search } = + window.arguments[0]; + + this._telemetryId = telemetryId; + + // This is a list of available locales that the user selected. It's more + // restricted than the Intl notion of `requested` as it only contains + // locale codes for which we have matching locales available. + // The first time this dialog is opened, populate with appLocalesAsBCP47. + let selectedLocales = + selectedLocalesForRestart || Services.locale.appLocalesAsBCP47; + let selectedLocaleSet = new Set(selectedLocales); + let available = await LangPackMatcher.getAvailableLocales(); + let availableSet = new Set(available); + + // Filter selectedLocales since the user may select a locale when it is + // available and then disable it. + selectedLocales = selectedLocales.filter(locale => + availableSet.has(locale) + ); + // Nothing in available should be in selectedSet. + available = available.filter(locale => !selectedLocaleSet.has(locale)); + + await this.initSelectedLocales(selectedLocales); + await this.initAvailableLocales(available, search); + + this.initialized = true; + + // Now the component is initialized, it's safe to accept the results. + document + .getElementById("BrowserLanguagesDialog") + .addEventListener("beforeaccept", () => { + this.selected = this._selectedLocalesUI.items.map(item => item.value); + }); + }, + + /** + * @param {string[]} selectedLocales - BCP 47 locale identifiers + */ + async initSelectedLocales(selectedLocales) { + this._selectedLocalesUI = new OrderedListBox({ + richlistbox: document.getElementById("selectedLocales"), + upButton: document.getElementById("up"), + downButton: document.getElementById("down"), + removeButton: document.getElementById("remove"), + onRemove: item => this.selectedLocaleRemoved(item), + onReorder: () => this.recordTelemetry("reorder"), + }); + this._selectedLocalesUI.setItems( + await getLocaleDisplayInfo(selectedLocales) + ); + }, + + /** + * @param {Set<string>} available - The set of available BCP 47 locale identifiers. + * @param {boolean} search - Whether the user opened this from "Search for more + * languages" option. + */ + async initAvailableLocales(available, search) { + this._availableLocalesUI = new SortedItemSelectList({ + menulist: document.getElementById("availableLocales"), + button: document.getElementById("add"), + compareFn: compareItems, + onSelect: item => this.availableLanguageSelected(item), + onChange: item => { + this.hideError(); + if (item.value == "search") { + // Record the search event here so we don't track the search from + // the main preferences pane twice. + this.recordTelemetry("search"); + this.loadLocalesFromAMO(); + } + }, + }); + + // Populate the list with the installed locales even if the user is + // searching in case the download fails. + await this.loadLocalesFromInstalled(available); + + // If the user opened this from the "Search for more languages" option, + // search AMO for available locales. + if (search) { + return this.loadLocalesFromAMO(); + } + + return undefined; + }, + + async loadLocalesFromAMO() { + if (!this.downloadEnabled) { + return; + } + + // Disable the dropdown while we hit the network. + this._availableLocalesUI.disableWithMessageId( + "browser-languages-searching" + ); + + // Fetch the available langpacks from AMO. + let availableLangpacks; + try { + availableLangpacks = await AddonRepository.getAvailableLangpacks(); + } catch (e) { + this.showError(); + return; + } + + // Store the available langpack info for later use. + this.availableLangpacks = new Map(); + for (let { target_locale, url, hash } of availableLangpacks) { + this.availableLangpacks.set(target_locale, { url, hash }); + } + + // Remove the installed locales from the available ones. + let installedLocales = new Set(await LangPackMatcher.getAvailableLocales()); + let notInstalledLocales = availableLangpacks + .filter(({ target_locale }) => !installedLocales.has(target_locale)) + .map(lang => lang.target_locale); + + // Create the rows for the remote locales. + let availableItems = await getLocaleDisplayInfo(notInstalledLocales); + availableItems.push({ + label: await document.l10n.formatValue( + "browser-languages-available-label" + ), + className: "label-item", + disabled: true, + installed: false, + }); + + // Remove the search option and add the remote locales. + let items = this._availableLocalesUI.items; + items.pop(); + items = items.concat(availableItems); + + // Update the dropdown and enable it again. + this._availableLocalesUI.setItems(items); + this._availableLocalesUI.enableWithMessageId( + "browser-languages-select-language" + ); + }, + + /** + * @param {Set<string>} available - The set of available (BCP 47) locales. + */ + async loadLocalesFromInstalled(available) { + let items; + if (available.length) { + items = await getLocaleDisplayInfo(available); + items.push(await this.createInstalledLabel()); + } else { + items = []; + } + if (this.downloadEnabled) { + items.push({ + label: await document.l10n.formatValue("browser-languages-search"), + value: "search", + }); + } + this._availableLocalesUI.setItems(items); + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async availableLanguageSelected(item) { + if ((await LangPackMatcher.getAvailableLocales()).includes(item.value)) { + this.recordTelemetry("add"); + await this.requestLocalLanguage(item); + } else if (this.availableLangpacks.has(item.value)) { + // Telemetry is tracked in requestRemoteLanguage. + await this.requestRemoteLanguage(item); + } else { + this.showError(); + } + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async requestLocalLanguage(item) { + this._selectedLocalesUI.addItem(item); + let selectedCount = this._selectedLocalesUI.items.length; + let availableCount = (await LangPackMatcher.getAvailableLocales()).length; + if (selectedCount == availableCount) { + // Remove the installed label, they're all installed. + this._availableLocalesUI.items.shift(); + this._availableLocalesUI.setItems(this._availableLocalesUI.items); + } + // The label isn't always reset when the selected item is removed, so set it again. + this._availableLocalesUI.enableWithMessageId( + "browser-languages-select-language" + ); + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async requestRemoteLanguage(item) { + this._availableLocalesUI.disableWithMessageId( + "browser-languages-downloading" + ); + + let { url, hash } = this.availableLangpacks.get(item.value); + let addon; + + try { + addon = await installFromUrl(url, hash, installId => + this.recordTelemetry("add", { installId }) + ); + } catch (e) { + this.showError(); + return; + } + + // If the add-on was previously installed, it might be disabled still. + if (addon.userDisabled) { + await addon.enable(); + } + + item.installed = true; + this._selectedLocalesUI.addItem(item); + this._availableLocalesUI.enableWithMessageId( + "browser-languages-select-language" + ); + + // This is an async task that will install the recommended dictionaries for + // this locale. This will fail silently at least until a management UI is + // added in bug 1493705. + this.installDictionariesForLanguage(item.value); + }, + + /** + * @param {string} locale The BCP 47 locale identifier + */ + async installDictionariesForLanguage(locale) { + try { + let ids = await dictionaryIdsForLocale(locale); + let addonInfos = await AddonRepository.getAddonsByIDs(ids); + await Promise.all( + addonInfos.map(info => installFromUrl(info.sourceURI.spec)) + ); + } catch (e) { + console.error(e); + } + }, + + showError() { + document.getElementById("warning-message").hidden = false; + this._availableLocalesUI.enableWithMessageId( + "browser-languages-select-language" + ); + + // The height has likely changed, find our SubDialog and tell it to resize. + requestAnimationFrame(() => { + let dialogs = window.opener.gSubDialog._dialogs; + let index = dialogs.findIndex(d => d._frame.contentDocument == document); + if (index != -1) { + dialogs[index].resizeDialog(); + } + }); + }, + + hideError() { + document.getElementById("warning-message").hidden = true; + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async selectedLocaleRemoved(item) { + this.recordTelemetry("remove"); + + this._availableLocalesUI.addItem(item); + + // If the item we added is at the top of the list, it needs the label. + if (this._availableLocalesUI.items[0] == item) { + this._availableLocalesUI.addItem(await this.createInstalledLabel()); + } + }, + + async createInstalledLabel() { + return { + label: await document.l10n.formatValue( + "browser-languages-installed-label" + ), + className: "label-item", + disabled: true, + installed: true, + }; + }, +}; diff --git a/browser/components/preferences/dialogs/browserLanguages.xhtml b/browser/components/preferences/dialogs/browserLanguages.xhtml new file mode 100644 index 0000000000..3818ccc058 --- /dev/null +++ b/browser/components/preferences/dialogs/browserLanguages.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="browser-languages-window2" + data-l10n-attrs="title, style" + onload="gBrowserLanguagesDialog.onLoad();" +> + <dialog + id="BrowserLanguagesDialog" + buttons="accept,cancel,help" + helpTopic="change-language" + > + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link rel="localization" href="browser/preferences/languages.ftl" /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://browser/content/preferences/dialogs/browserLanguages.js" /> + + <description data-l10n-id="browser-languages-description" /> + + <box class="languages-grid"> + <richlistbox id="selectedLocales" /> + <vbox> + <button + id="up" + class="action-button" + disabled="true" + data-l10n-id="languages-customize-moveup" + /> + <button + id="down" + class="action-button" + disabled="true" + data-l10n-id="languages-customize-movedown" + /> + <button + id="remove" + class="action-button" + disabled="true" + data-l10n-id="languages-customize-remove" + /> + </vbox> + + <menulist + id="availableLocales" + class="available-locales-list" + data-l10n-id="browser-languages-select-language" + data-l10n-attrs="placeholder,label" + > + <menupopup /> + </menulist> + <button + id="add" + class="add-browser-language action-button" + data-l10n-id="languages-customize-add" + disabled="true" + /> + </box> + <hbox + id="warning-message" + class="message-bar message-bar-warning" + hidden="true" + > + <image class="message-bar-icon" /> + <description + class="message-bar-description" + data-l10n-id="browser-languages-error" + /> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/clearSiteData.css b/browser/components/preferences/dialogs/clearSiteData.css new file mode 100644 index 0000000000..5b1f5bbe2d --- /dev/null +++ b/browser/components/preferences/dialogs/clearSiteData.css @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.options-container { + background-color: var(--in-content-box-background); + border: 1px solid var(--in-content-box-border-color); + border-radius: 2px; + color: var(--in-content-text-color); + padding: 0.5em; +} + +.option { + padding-bottom: 8px; +} + +.option-description { + color: var(--text-color-deemphasized); + margin-top: -0.5em !important; +} diff --git a/browser/components/preferences/dialogs/clearSiteData.js b/browser/components/preferences/dialogs/clearSiteData.js new file mode 100644 index 0000000000..eae2d5f772 --- /dev/null +++ b/browser/components/preferences/dialogs/clearSiteData.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { SiteDataManager } = ChromeUtils.import( + "resource:///modules/SiteDataManager.jsm" +); + +ChromeUtils.defineESModuleGetters(this, { + DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", +}); + +var gClearSiteDataDialog = { + _clearSiteDataCheckbox: null, + _clearCacheCheckbox: null, + + onLoad() { + document.mozSubdialogReady = this.init(); + }, + + async init() { + this._dialog = document.querySelector("dialog"); + this._clearSiteDataCheckbox = document.getElementById("clearSiteData"); + this._clearCacheCheckbox = document.getElementById("clearCache"); + + // We'll block init() on this because the result values may impact + // subdialog sizing. + await Promise.all([ + SiteDataManager.getTotalUsage().then(bytes => { + let [amount, unit] = DownloadUtils.convertByteUnits(bytes); + document.l10n.setAttributes( + this._clearSiteDataCheckbox, + "clear-site-data-cookies-with-data", + { amount, unit } + ); + }), + SiteDataManager.getCacheSize().then(bytes => { + let [amount, unit] = DownloadUtils.convertByteUnits(bytes); + document.l10n.setAttributes( + this._clearCacheCheckbox, + "clear-site-data-cache-with-data", + { amount, unit } + ); + }), + ]); + await document.l10n.translateElements([ + this._clearCacheCheckbox, + this._clearSiteDataCheckbox, + ]); + + document.addEventListener("dialogaccept", event => this.onClear(event)); + + this._clearSiteDataCheckbox.addEventListener("command", e => + this.onCheckboxCommand(e) + ); + this._clearCacheCheckbox.addEventListener("command", e => + this.onCheckboxCommand(e) + ); + }, + + onCheckboxCommand(event) { + this._dialog.setAttribute( + "buttondisabledaccept", + !(this._clearSiteDataCheckbox.checked || this._clearCacheCheckbox.checked) + ); + }, + + onClear(event) { + let clearSiteData = this._clearSiteDataCheckbox.checked; + let clearCache = this._clearCacheCheckbox.checked; + + if (clearSiteData) { + // Ask for confirmation before clearing site data + if (!SiteDataManager.promptSiteDataRemoval(window)) { + clearSiteData = false; + // Prevent closing the dialog when the data removal wasn't allowed. + event.preventDefault(); + } + } + + if (clearSiteData) { + SiteDataManager.removeSiteData(); + } + if (clearCache) { + SiteDataManager.removeCache(); + + // If we're not clearing site data, we need to tell the + // SiteDataManager to signal that it's updating. + if (!clearSiteData) { + SiteDataManager.updateSites(); + } + } + }, +}; + +window.addEventListener("load", () => gClearSiteDataDialog.onLoad()); diff --git a/browser/components/preferences/dialogs/clearSiteData.xhtml b/browser/components/preferences/dialogs/clearSiteData.xhtml new file mode 100644 index 0000000000..574dc4fb5d --- /dev/null +++ b/browser/components/preferences/dialogs/clearSiteData.xhtml @@ -0,0 +1,70 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/clearSiteData.css" type="text/css"?> + +<window + id="ClearSiteDataDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="clear-site-data-window2" + data-l10n-attrs="title, style" + persist="width height" +> + <dialog + buttons="accept,cancel" + data-l10n-id="clear-site-data-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="browser/preferences/clearSiteData.ftl" + /> + </linkset> + <script src="chrome://browser/content/preferences/dialogs/clearSiteData.js" /> + + <keyset> + <key + data-l10n-id="clear-site-data-close-key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane"> + <description control="url" data-l10n-id="clear-site-data-description" /> + <separator class="thin" /> + <vbox class="options-container"> + <vbox class="option"> + <checkbox + data-l10n-id="clear-site-data-cookies-empty" + id="clearSiteData" + checked="true" + /> + <description + class="option-description indent" + data-l10n-id="clear-site-data-cookies-info" + /> + </vbox> + <vbox class="option"> + <checkbox + data-l10n-id="clear-site-data-cache-empty" + id="clearCache" + checked="true" + /> + <description + class="option-description indent" + data-l10n-id="clear-site-data-cache-info" + /> + </vbox> + </vbox> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/colors.js b/browser/components/preferences/dialogs/colors.js new file mode 100644 index 0000000000..3bb78e5ec1 --- /dev/null +++ b/browser/components/preferences/dialogs/colors.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +document + .getElementById("ColorsDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); + +Preferences.addAll([ + { id: "browser.display.document_color_use", type: "int" }, + { id: "browser.anchor_color", type: "string" }, + { id: "browser.visited_color", type: "string" }, + { id: "browser.underline_anchors", type: "bool" }, + { id: "browser.display.foreground_color", type: "string" }, + { id: "browser.display.background_color", type: "string" }, + { id: "browser.display.use_system_colors", type: "bool" }, +]); diff --git a/browser/components/preferences/dialogs/colors.xhtml b/browser/components/preferences/dialogs/colors.xhtml new file mode 100644 index 0000000000..8d81273880 --- /dev/null +++ b/browser/components/preferences/dialogs/colors.xhtml @@ -0,0 +1,140 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="colors-dialog2" + data-l10n-attrs="title, style" + persist="lastSelected" +> + <dialog + id="ColorsDialog" + buttons="accept,cancel,help" + helpTopic="prefs-fonts-and-colors" + > + <linkset> + <html:link rel="localization" href="browser/preferences/colors.ftl" /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + + <keyset> + <key + data-l10n-id="colors-close-key" + modifiers="accel" + oncommand="Preferences.close(event)" + /> + </keyset> + + <hbox> + <groupbox flex="1"> + <label><html:h2 data-l10n-id="colors-text-and-background" /></label> + <hbox align="center"> + <label + data-l10n-id="colors-text-header" + control="foregroundtextmenu" + /> + <spacer flex="1" /> + <html:input + type="color" + id="foregroundtextmenu" + preference="browser.display.foreground_color" + /> + </hbox> + <hbox align="center" style="margin-top: 5px"> + <label data-l10n-id="colors-background" control="backgroundmenu" /> + <spacer flex="1" /> + <html:input + type="color" + id="backgroundmenu" + preference="browser.display.background_color" + /> + </hbox> + <separator class="thin" /> + <hbox align="center"> + <checkbox + id="browserUseSystemColors" + data-l10n-id="colors-use-system" + preference="browser.display.use_system_colors" + /> + </hbox> + </groupbox> + + <groupbox flex="1"> + <label><html:h2 data-l10n-id="colors-links-header" /></label> + <hbox align="center"> + <label + data-l10n-id="colors-unvisited-links" + control="unvisitedlinkmenu" + /> + <spacer flex="1" /> + <html:input + type="color" + id="unvisitedlinkmenu" + preference="browser.anchor_color" + /> + </hbox> + <hbox align="center" style="margin-top: 5px"> + <label + data-l10n-id="colors-visited-links" + control="visitedlinkmenu" + /> + <spacer flex="1" /> + <html:input + type="color" + id="visitedlinkmenu" + preference="browser.visited_color" + /> + </hbox> + <separator class="thin" /> + <hbox align="center"> + <checkbox + id="browserUnderlineAnchors" + data-l10n-id="colors-underline-links" + preference="browser.underline_anchors" + /> + </hbox> + </groupbox> + </hbox> + + <label data-l10n-id="colors-page-override" control="useDocumentColors" /> + <hbox> + <menulist + id="useDocumentColors" + preference="browser.display.document_color_use" + flex="1" + > + <menupopup> + <menuitem + data-l10n-id="colors-page-override-option-always" + value="2" + id="documentColorAlways" + /> + <menuitem + data-l10n-id="colors-page-override-option-auto" + value="0" + id="documentColorAutomatic" + /> + <menuitem + data-l10n-id="colors-page-override-option-never" + value="1" + id="documentColorNever" + /> + </menupopup> + </menulist> + </hbox> + + <!-- Load the script after the elements for layout issues (bug 1501755). --> + <script src="chrome://browser/content/preferences/dialogs/colors.js" /> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/connection.js b/browser/components/preferences/dialogs/connection.js new file mode 100644 index 0000000000..ee669b3762 --- /dev/null +++ b/browser/components/preferences/dialogs/connection.js @@ -0,0 +1,381 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /browser/base/content/utilityOverlay.js */ +/* import-globals-from /toolkit/content/preferencesBindings.js */ +/* import-globals-from ../extensionControlled.js */ + +document + .getElementById("ConnectionsDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); + +Preferences.addAll([ + // Add network.proxy.autoconfig_url before network.proxy.type so they're + // both initialized when network.proxy.type initialization triggers a call to + // gConnectionsDialog.updateReloadButton(). + { id: "network.proxy.autoconfig_url", type: "string" }, + { id: "network.proxy.type", type: "int" }, + { id: "network.proxy.http", type: "string" }, + { id: "network.proxy.http_port", type: "int" }, + { id: "network.proxy.ssl", type: "string" }, + { id: "network.proxy.ssl_port", type: "int" }, + { id: "network.proxy.socks", type: "string" }, + { id: "network.proxy.socks_port", type: "int" }, + { id: "network.proxy.socks_version", type: "int" }, + { id: "network.proxy.socks_remote_dns", type: "bool" }, + { id: "network.proxy.no_proxies_on", type: "string" }, + { id: "network.proxy.share_proxy_settings", type: "bool" }, + { id: "signon.autologin.proxy", type: "bool" }, + { id: "pref.advanced.proxies.disable_button.reload", type: "bool" }, + { id: "network.proxy.backup.ssl", type: "string" }, + { id: "network.proxy.backup.ssl_port", type: "int" }, +]); + +window.addEventListener( + "DOMContentLoaded", + () => { + Preferences.get("network.proxy.type").on( + "change", + gConnectionsDialog.proxyTypeChanged.bind(gConnectionsDialog) + ); + Preferences.get("network.proxy.socks_version").on( + "change", + gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog) + ); + + document + .getElementById("disableProxyExtension") + .addEventListener( + "command", + makeDisableControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).bind( + gConnectionsDialog + ) + ); + gConnectionsDialog.updateProxySettingsUI(); + initializeProxyUI(gConnectionsDialog); + gConnectionsDialog.registerSyncPrefListeners(); + document + .getElementById("ConnectionsDialog") + .addEventListener("beforeaccept", e => + gConnectionsDialog.beforeAccept(e) + ); + }, + { once: true, capture: true } +); + +var gConnectionsDialog = { + beforeAccept(event) { + var proxyTypePref = Preferences.get("network.proxy.type"); + if (proxyTypePref.value == 2) { + this.doAutoconfigURLFixup(); + return; + } + + if (proxyTypePref.value != 1) { + return; + } + + var httpProxyURLPref = Preferences.get("network.proxy.http"); + var httpProxyPortPref = Preferences.get("network.proxy.http_port"); + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + + // If the port is 0 and the proxy server is specified, focus on the port and cancel submission. + for (let prefName of ["http", "ssl", "socks"]) { + let proxyPortPref = Preferences.get( + "network.proxy." + prefName + "_port" + ); + let proxyPref = Preferences.get("network.proxy." + prefName); + // Only worry about ports which are currently active. If the share option is on, then ignore + // all ports except the HTTP and SOCKS port + if ( + proxyPref.value != "" && + proxyPortPref.value == 0 && + (prefName == "http" || prefName == "socks" || !shareProxiesPref.value) + ) { + document + .getElementById("networkProxy" + prefName.toUpperCase() + "_Port") + .focus(); + event.preventDefault(); + return; + } + } + + // In the case of a shared proxy preference, backup the current values and update with the HTTP value + if (shareProxiesPref.value) { + var proxyServerURLPref = Preferences.get("network.proxy.ssl"); + var proxyPortPref = Preferences.get("network.proxy.ssl_port"); + var backupServerURLPref = Preferences.get("network.proxy.backup.ssl"); + var backupPortPref = Preferences.get("network.proxy.backup.ssl_port"); + backupServerURLPref.value = + backupServerURLPref.value || proxyServerURLPref.value; + backupPortPref.value = backupPortPref.value || proxyPortPref.value; + proxyServerURLPref.value = httpProxyURLPref.value; + proxyPortPref.value = httpProxyPortPref.value; + } + + this.sanitizeNoProxiesPref(); + }, + + checkForSystemProxy() { + if ("@mozilla.org/system-proxy-settings;1" in Cc) { + document.getElementById("systemPref").removeAttribute("hidden"); + } + }, + + proxyTypeChanged() { + var proxyTypePref = Preferences.get("network.proxy.type"); + + // Update http + var httpProxyURLPref = Preferences.get("network.proxy.http"); + httpProxyURLPref.updateControlDisabledState(proxyTypePref.value != 1); + var httpProxyPortPref = Preferences.get("network.proxy.http_port"); + httpProxyPortPref.updateControlDisabledState(proxyTypePref.value != 1); + + // Now update the other protocols + this.updateProtocolPrefs(); + + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + shareProxiesPref.updateControlDisabledState(proxyTypePref.value != 1); + var autologinProxyPref = Preferences.get("signon.autologin.proxy"); + autologinProxyPref.updateControlDisabledState(proxyTypePref.value == 0); + var noProxiesPref = Preferences.get("network.proxy.no_proxies_on"); + noProxiesPref.updateControlDisabledState(proxyTypePref.value == 0); + + var autoconfigURLPref = Preferences.get("network.proxy.autoconfig_url"); + autoconfigURLPref.updateControlDisabledState(proxyTypePref.value != 2); + + this.updateReloadButton(); + + document.getElementById("networkProxyNoneLocalhost").hidden = + Services.prefs.getBoolPref( + "network.proxy.allow_hijacking_localhost", + false + ); + }, + + updateDNSPref() { + var socksVersionPref = Preferences.get("network.proxy.socks_version"); + var socksDNSPref = Preferences.get("network.proxy.socks_remote_dns"); + var proxyTypePref = Preferences.get("network.proxy.type"); + var isDefinitelySocks4 = + proxyTypePref.value == 1 && socksVersionPref.value == 4; + socksDNSPref.updateControlDisabledState( + isDefinitelySocks4 || proxyTypePref.value == 0 + ); + return undefined; + }, + + updateReloadButton() { + // Disable the "Reload PAC" button if the selected proxy type is not PAC or + // if the current value of the PAC input does not match the value stored + // in prefs. Likewise, disable the reload button if PAC is not configured + // in prefs. + + var typedURL = document.getElementById("networkProxyAutoconfigURL").value; + var proxyTypeCur = Preferences.get("network.proxy.type").value; + + var pacURL = Services.prefs.getCharPref("network.proxy.autoconfig_url"); + var proxyType = Services.prefs.getIntPref("network.proxy.type"); + + var disableReloadPref = Preferences.get( + "pref.advanced.proxies.disable_button.reload" + ); + disableReloadPref.updateControlDisabledState( + proxyTypeCur != 2 || proxyType != 2 || typedURL != pacURL + ); + }, + + readProxyType() { + this.proxyTypeChanged(); + return undefined; + }, + + updateProtocolPrefs() { + var proxyTypePref = Preferences.get("network.proxy.type"); + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + var proxyPrefs = ["ssl", "socks"]; + for (var i = 0; i < proxyPrefs.length; ++i) { + var proxyServerURLPref = Preferences.get( + "network.proxy." + proxyPrefs[i] + ); + var proxyPortPref = Preferences.get( + "network.proxy." + proxyPrefs[i] + "_port" + ); + + // Restore previous per-proxy custom settings, if present. + if (proxyPrefs[i] != "socks" && !shareProxiesPref.value) { + var backupServerURLPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + ); + var backupPortPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + "_port" + ); + if (backupServerURLPref.hasUserValue) { + proxyServerURLPref.value = backupServerURLPref.value; + backupServerURLPref.reset(); + } + if (backupPortPref.hasUserValue) { + proxyPortPref.value = backupPortPref.value; + backupPortPref.reset(); + } + } + + proxyServerURLPref.updateElements(); + proxyPortPref.updateElements(); + let prefIsShared = proxyPrefs[i] != "socks" && shareProxiesPref.value; + proxyServerURLPref.updateControlDisabledState( + proxyTypePref.value != 1 || prefIsShared + ); + proxyPortPref.updateControlDisabledState( + proxyTypePref.value != 1 || prefIsShared + ); + } + var socksVersionPref = Preferences.get("network.proxy.socks_version"); + socksVersionPref.updateControlDisabledState(proxyTypePref.value != 1); + this.updateDNSPref(); + return undefined; + }, + + readProxyProtocolPref(aProtocol, aIsPort) { + if (aProtocol != "socks") { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + var pref = Preferences.get( + "network.proxy.http" + (aIsPort ? "_port" : "") + ); + return pref.value; + } + + var backupPref = Preferences.get( + "network.proxy.backup." + aProtocol + (aIsPort ? "_port" : "") + ); + return backupPref.hasUserValue ? backupPref.value : undefined; + } + return undefined; + }, + + reloadPAC() { + Cc["@mozilla.org/network/protocol-proxy-service;1"] + .getService() + .reloadPAC(); + }, + + doAutoconfigURLFixup() { + var autoURL = document.getElementById("networkProxyAutoconfigURL"); + var autoURLPref = Preferences.get("network.proxy.autoconfig_url"); + try { + autoURLPref.value = autoURL.value = Services.uriFixup.getFixupURIInfo( + autoURL.value + ).preferredURI.spec; + } catch (ex) {} + }, + + sanitizeNoProxiesPref() { + var noProxiesPref = Preferences.get("network.proxy.no_proxies_on"); + // replace substrings of ; and \n with commas if they're neither immediately + // preceded nor followed by a valid separator character + noProxiesPref.value = noProxiesPref.value.replace( + /([^, \n;])[;\n]+(?![,\n;])/g, + "$1," + ); + // replace any remaining ; and \n since some may follow commas, etc. + noProxiesPref.value = noProxiesPref.value.replace(/[;\n]/g, ""); + }, + + readHTTPProxyServer() { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + this.updateProtocolPrefs(); + } + return undefined; + }, + + readHTTPProxyPort() { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + this.updateProtocolPrefs(); + } + return undefined; + }, + + getProxyControls() { + let controlGroup = document.getElementById("networkProxyType"); + return [ + ...controlGroup.querySelectorAll(":scope > radio"), + ...controlGroup.querySelectorAll("label"), + ...controlGroup.querySelectorAll("input"), + ...controlGroup.querySelectorAll("checkbox"), + ...document.querySelectorAll("#networkProxySOCKSVersion > radio"), + ...document.querySelectorAll("#ConnectionsDialogPane > checkbox"), + ]; + }, + + // Update the UI to show/hide the extension controlled message for + // proxy settings. + async updateProxySettingsUI() { + let isLocked = API_PROXY_PREFS.some(pref => + Services.prefs.prefIsLocked(pref) + ); + + function setInputsDisabledState(isControlled) { + for (let element of gConnectionsDialog.getProxyControls()) { + element.disabled = isControlled; + } + gConnectionsDialog.proxyTypeChanged(); + } + + if (isLocked) { + // An extension can't control this setting if any pref is locked. + hideControllingExtension(PROXY_KEY); + } else { + handleControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).then( + setInputsDisabledState + ); + } + }, + + registerSyncPrefListeners() { + function setSyncFromPrefListener(element_id, callback) { + Preferences.addSyncFromPrefListener( + document.getElementById(element_id), + callback + ); + } + setSyncFromPrefListener("networkProxyType", () => this.readProxyType()); + setSyncFromPrefListener("networkProxyHTTP", () => + this.readHTTPProxyServer() + ); + setSyncFromPrefListener("networkProxyHTTP_Port", () => + this.readHTTPProxyPort() + ); + setSyncFromPrefListener("shareAllProxies", () => + this.updateProtocolPrefs() + ); + setSyncFromPrefListener("networkProxySSL", () => + this.readProxyProtocolPref("ssl", false) + ); + setSyncFromPrefListener("networkProxySSL_Port", () => + this.readProxyProtocolPref("ssl", true) + ); + setSyncFromPrefListener("networkProxySOCKS", () => + this.readProxyProtocolPref("socks", false) + ); + setSyncFromPrefListener("networkProxySOCKS_Port", () => + this.readProxyProtocolPref("socks", true) + ); + }, +}; diff --git a/browser/components/preferences/dialogs/connection.xhtml b/browser/components/preferences/dialogs/connection.xhtml new file mode 100644 index 0000000000..4f161a710d --- /dev/null +++ b/browser/components/preferences/dialogs/connection.xhtml @@ -0,0 +1,244 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="connection-window2" + data-l10n-attrs="title, style" + persist="lastSelected" + onload="gConnectionsDialog.checkForSystemProxy();" +> + <dialog + id="ConnectionsDialog" + buttons="accept,cancel,help" + helpTopic="prefs-connection-settings" + > + <!-- Used for extension-controlled lockdown message --> + <linkset> + <html:link rel="localization" href="browser/preferences/connection.ftl" /> + <html:link + rel="localization" + href="browser/preferences/preferences.ftl" + /> + <html:link rel="localization" href="branding/brand.ftl" /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://browser/content/preferences/extensionControlled.js" /> + + <keyset> + <key + data-l10n-id="connection-close-key" + modifiers="accel" + oncommand="Preferences.close(event)" + /> + </keyset> + + <script src="chrome://browser/content/preferences/dialogs/connection.js" /> + + <hbox + id="proxyExtensionContent" + align="start" + hidden="true" + class="extension-controlled" + > + <description control="disableProxyExtension" flex="1" /> + <button + id="disableProxyExtension" + class="extension-controlled-button accessory-button" + data-l10n-id="connection-disable-extension" + /> + </hbox> + + <groupbox> + <label><html:h2 data-l10n-id="connection-proxy-configure" /></label> + + <radiogroup id="networkProxyType" preference="network.proxy.type"> + <radio value="0" data-l10n-id="connection-proxy-option-no" /> + <radio value="4" data-l10n-id="connection-proxy-option-auto" /> + <radio + value="5" + data-l10n-id="connection-proxy-option-system" + id="systemPref" + hidden="true" + /> + <radio value="1" data-l10n-id="connection-proxy-option-manual" /> + <box id="proxy-grid" class="indent" flex="1"> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label + data-l10n-id="connection-proxy-http" + control="networkProxyHTTP" + /> + </hbox> + <hbox align="center"> + <html:input + id="networkProxyHTTP" + type="text" + style="flex: 1" + preference="network.proxy.http" + /> + <label + data-l10n-id="connection-proxy-http-port" + control="networkProxyHTTP_Port" + /> + <html:input + id="networkProxyHTTP_Port" + class="proxy-port-input" + hidespinbuttons="true" + type="number" + min="0" + max="65535" + preference="network.proxy.http_port" + /> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <hbox /> + <hbox> + <checkbox + id="shareAllProxies" + data-l10n-id="connection-proxy-https-sharing" + preference="network.proxy.share_proxy_settings" + /> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label + data-l10n-id="connection-proxy-https" + control="networkProxySSL" + /> + </hbox> + <hbox align="center"> + <html:input + id="networkProxySSL" + type="text" + style="flex: 1" + preference="network.proxy.ssl" + /> + <label + data-l10n-id="connection-proxy-ssl-port" + control="networkProxySSL_Port" + /> + <html:input + id="networkProxySSL_Port" + class="proxy-port-input" + hidespinbuttons="true" + type="number" + min="0" + max="65535" + size="5" + preference="network.proxy.ssl_port" + /> + </hbox> + </html:div> + <separator class="thin" /> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label + data-l10n-id="connection-proxy-socks" + control="networkProxySOCKS" + /> + </hbox> + <hbox align="center"> + <html:input + id="networkProxySOCKS" + type="text" + style="flex: 1" + preference="network.proxy.socks" + /> + <label + data-l10n-id="connection-proxy-socks-port" + control="networkProxySOCKS_Port" + /> + <html:input + id="networkProxySOCKS_Port" + class="proxy-port-input" + hidespinbuttons="true" + type="number" + min="0" + max="65535" + size="5" + preference="network.proxy.socks_port" + /> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <spacer /> + <box pack="start"> + <radiogroup + id="networkProxySOCKSVersion" + orient="horizontal" + preference="network.proxy.socks_version" + > + <radio + id="networkProxySOCKSVersion4" + value="4" + data-l10n-id="connection-proxy-socks4" + /> + <radio + id="networkProxySOCKSVersion5" + value="5" + data-l10n-id="connection-proxy-socks5" + /> + </radiogroup> + </box> + </html:div> + </box> + <radio value="2" data-l10n-id="connection-proxy-autotype" /> + <hbox class="indent" flex="1" align="center"> + <html:input + id="networkProxyAutoconfigURL" + type="text" + style="flex: 1" + preference="network.proxy.autoconfig_url" + oninput="gConnectionsDialog.updateReloadButton();" + /> + <button + id="autoReload" + data-l10n-id="connection-proxy-reload" + oncommand="gConnectionsDialog.reloadPAC();" + preference="pref.advanced.proxies.disable_button.reload" + /> + </hbox> + </radiogroup> + </groupbox> + <separator class="thin" /> + <label data-l10n-id="connection-proxy-noproxy" control="networkProxyNone" /> + <html:textarea + id="networkProxyNone" + preference="network.proxy.no_proxies_on" + rows="2" + /> + <label + control="networkProxyNone" + data-l10n-id="connection-proxy-noproxy-desc" + /> + <label + id="networkProxyNoneLocalhost" + control="networkProxyNone" + data-l10n-id="connection-proxy-noproxy-localhost-desc-2" + /> + <separator class="thin" /> + <checkbox + id="autologinProxy" + data-l10n-id="connection-proxy-autologin" + preference="signon.autologin.proxy" + /> + <checkbox + id="networkProxySOCKSRemoteDNS" + preference="network.proxy.socks_remote_dns" + data-l10n-id="connection-proxy-socks-remote-dns" + /> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/containers.js b/browser/components/preferences/dialogs/containers.js new file mode 100644 index 0000000000..14526545b6 --- /dev/null +++ b/browser/components/preferences/dialogs/containers.js @@ -0,0 +1,167 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { ContextualIdentityService } = ChromeUtils.importESModule( + "resource://gre/modules/ContextualIdentityService.sys.mjs" +); + +/** + * We want to set the window title immediately to prevent flickers. + */ +function setTitle() { + let params = window.arguments[0] || {}; + + let winElem = document.documentElement; + if (params.userContextId) { + document.l10n.setAttributes(winElem, "containers-window-update-settings2", { + name: params.identity.name, + }); + } else { + document.l10n.setAttributes(winElem, "containers-window-new2"); + } +} +setTitle(); + +let gContainersManager = { + icons: [ + "fingerprint", + "briefcase", + "dollar", + "cart", + "vacation", + "gift", + "food", + "fruit", + "pet", + "tree", + "chill", + "circle", + "fence", + ], + + colors: [ + "blue", + "turquoise", + "green", + "yellow", + "orange", + "red", + "pink", + "purple", + "toolbar", + ], + + onLoad() { + let params = window.arguments[0] || {}; + this.init(params); + }, + + init(aParams) { + this._dialog = document.querySelector("dialog"); + this.userContextId = aParams.userContextId || null; + this.identity = aParams.identity; + + const iconWrapper = document.getElementById("iconWrapper"); + iconWrapper.appendChild(this.createIconButtons()); + + const colorWrapper = document.getElementById("colorWrapper"); + colorWrapper.appendChild(this.createColorSwatches()); + + if (this.identity.name) { + const name = document.getElementById("name"); + name.value = this.identity.name; + this.checkForm(); + } + + document.addEventListener("dialogaccept", () => this.onApplyChanges()); + + // This is to prevent layout jank caused by the svgs and outlines rendering at different times + document.getElementById("containers-content").removeAttribute("hidden"); + }, + + uninit() {}, + + // Check if name is provided to determine if the form can be submitted + checkForm() { + const name = document.getElementById("name"); + this._dialog.setAttribute("buttondisabledaccept", !name.value.trim()); + }, + + createIconButtons(defaultIcon) { + let radiogroup = document.createXULElement("radiogroup"); + radiogroup.setAttribute("id", "icon"); + radiogroup.className = "icon-buttons radio-buttons"; + + for (let icon of this.icons) { + let iconSwatch = document.createXULElement("radio"); + iconSwatch.id = "iconbutton-" + icon; + iconSwatch.name = "icon"; + iconSwatch.type = "radio"; + iconSwatch.value = icon; + + if (this.identity.icon && this.identity.icon == icon) { + iconSwatch.setAttribute("selected", true); + } + + document.l10n.setAttributes(iconSwatch, `containers-icon-${icon}`); + let iconElement = document.createXULElement("hbox"); + iconElement.className = "userContext-icon"; + iconElement.classList.add("identity-icon-" + icon); + + iconSwatch.appendChild(iconElement); + radiogroup.appendChild(iconSwatch); + } + + return radiogroup; + }, + + createColorSwatches(defaultColor) { + let radiogroup = document.createXULElement("radiogroup"); + radiogroup.setAttribute("id", "color"); + radiogroup.className = "radio-buttons"; + + for (let color of this.colors) { + let colorSwatch = document.createXULElement("radio"); + colorSwatch.id = "colorswatch-" + color; + colorSwatch.name = "color"; + colorSwatch.type = "radio"; + colorSwatch.value = color; + + if (this.identity.color && this.identity.color == color) { + colorSwatch.setAttribute("selected", true); + } + + document.l10n.setAttributes(colorSwatch, `containers-color-${color}`); + let iconElement = document.createXULElement("hbox"); + iconElement.className = "userContext-icon"; + iconElement.classList.add("identity-icon-circle"); + iconElement.classList.add("identity-color-" + color); + + colorSwatch.appendChild(iconElement); + radiogroup.appendChild(colorSwatch); + } + return radiogroup; + }, + + onApplyChanges() { + let icon = document.getElementById("icon").value; + let color = document.getElementById("color").value; + let name = document.getElementById("name").value; + + if (!this.icons.includes(icon)) { + throw new Error("Internal error. The icon value doesn't match."); + } + + if (!this.colors.includes(color)) { + throw new Error("Internal error. The color value doesn't match."); + } + + if (this.userContextId) { + ContextualIdentityService.update(this.userContextId, name, icon, color); + } else { + ContextualIdentityService.create(name, icon, color); + } + window.parent.location.reload(); + }, +}; diff --git a/browser/components/preferences/dialogs/containers.xhtml b/browser/components/preferences/dialogs/containers.xhtml new file mode 100644 index 0000000000..446a78aee0 --- /dev/null +++ b/browser/components/preferences/dialogs/containers.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/containers-dialog.css" type="text/css"?> + +<window + id="ContainersDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-attrs="title, style" + onload="gContainersManager.onLoad();" + onunload="gContainersManager.uninit();" + persist="width height" +> + <dialog + buttons="accept" + buttondisabledaccept="true" + data-l10n-id="containers-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="browser/preferences/containers.ftl" /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/containers.js" /> + + <keyset> + <key + data-l10n-id="containers-window-close" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane" hidden="true" id="containers-content"> + <hbox align="start"> + <label + id="nameLabel" + control="name" + data-l10n-id="containers-name-label" + data-l10n-attrs="style" + /> + <html:input + id="name" + type="text" + data-l10n-id="containers-name-text" + oninput="gContainersManager.checkForm();" + /> + </hbox> + <hbox align="center" id="colorWrapper"> + <label + id="colorLabel" + control="color" + data-l10n-id="containers-color-label" + data-l10n-attrs="style" + /> + </hbox> + <hbox align="center" id="iconWrapper"> + <label + id="iconLabel" + control="icon" + data-l10n-id="containers-icon-label" + data-l10n-attrs="style" + /> + </hbox> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/dohExceptions.js b/browser/components/preferences/dialogs/dohExceptions.js new file mode 100644 index 0000000000..f4c1d4d80d --- /dev/null +++ b/browser/components/preferences/dialogs/dohExceptions.js @@ -0,0 +1,287 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var gDoHExceptionsManager = { + _exceptions: new Set(), + _list: null, + _prefLocked: false, + + init() { + document.addEventListener("dialogaccept", () => this.onApplyChanges()); + + this._btnAddException = document.getElementById("btnAddException"); + this._removeButton = document.getElementById("removeException"); + this._removeAllButton = document.getElementById("removeAllExceptions"); + this._list = document.getElementById("permissionsBox"); + + this._urlField = document.getElementById("url"); + this.onExceptionInput(); + + this._loadExceptions(); + this.buildExceptionList(); + + this._urlField.focus(); + + this._prefLocked = Services.prefs.prefIsLocked( + "network.trr.excluded-domains" + ); + + this._btnAddException.disabled = this._prefLocked; + document.getElementById("exceptionDialog").getButton("accept").disabled = + this._prefLocked; + this._urlField.disabled = this._prefLocked; + }, + + _loadExceptions() { + let exceptionsFromPref = Services.prefs.getStringPref( + "network.trr.excluded-domains" + ); + + if (!exceptionsFromPref?.trim()) { + return; + } + + let exceptions = exceptionsFromPref.trim().split(","); + for (let exception of exceptions) { + let trimmed = exception.trim(); + if (trimmed) { + this._exceptions.add(trimmed); + } + } + }, + + addException() { + if (this._prefLocked) { + return; + } + + let textbox = document.getElementById("url"); + let inputValue = textbox.value.trim(); // trim any leading and trailing space + if (!inputValue.startsWith("http:") && !inputValue.startsWith("https:")) { + inputValue = `http://${inputValue}`; + } + let domain = ""; + try { + let uri = Services.io.newURI(inputValue); + domain = uri.host; + } catch (ex) { + document.l10n + .formatValues([ + { id: "permissions-invalid-uri-title" }, + { id: "permissions-invalid-uri-label" }, + ]) + .then(([title, message]) => { + Services.prompt.alert(window, title, message); + }); + return; + } + + if (!this._exceptions.has(domain)) { + this._exceptions.add(domain); + this.buildExceptionList(); + } + + textbox.value = ""; + textbox.focus(); + + // covers a case where the site exists already, so the buttons don't disable + this.onExceptionInput(); + + // enable "remove all" button as needed + this._setRemoveButtonState(); + }, + + onExceptionInput() { + this._btnAddException.disabled = !this._urlField.value; + }, + + onExceptionKeyPress(event) { + if (event.keyCode == KeyEvent.DOM_VK_RETURN) { + this._btnAddException.click(); + if (document.activeElement == this._urlField) { + event.preventDefault(); + } + } + }, + + onListBoxKeyPress(event) { + if (!this._list.selectedItem) { + return; + } + + if (this._prefLocked) { + return; + } + + if ( + event.keyCode == KeyEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + event.keyCode == KeyEvent.DOM_VK_BACK_SPACE) + ) { + this.onExceptionDelete(); + event.preventDefault(); + } + }, + + onListBoxSelect() { + this._setRemoveButtonState(); + }, + + _removeExceptionFromList(exception) { + this._exceptions.delete(exception); + let exceptionlistitem = document.getElementsByAttribute( + "domain", + exception + )[0]; + if (exceptionlistitem) { + exceptionlistitem.remove(); + } + }, + + onExceptionDelete() { + let richlistitem = this._list.selectedItem; + let exception = richlistitem.getAttribute("domain"); + + this._removeExceptionFromList(exception); + + this._setRemoveButtonState(); + }, + + onAllExceptionsDelete() { + for (let exception of this._exceptions.values()) { + this._removeExceptionFromList(exception); + } + + this._setRemoveButtonState(); + }, + + _createExceptionListItem(exception) { + let richlistitem = document.createXULElement("richlistitem"); + richlistitem.setAttribute("domain", exception); + let row = document.createXULElement("hbox"); + row.setAttribute("style", "flex: 1"); + + let hbox = document.createXULElement("hbox"); + let website = document.createXULElement("label"); + website.setAttribute("class", "website-name-value"); + website.setAttribute("value", exception); + hbox.setAttribute("class", "website-name"); + hbox.setAttribute("style", "flex: 3 3; width: 0"); + hbox.appendChild(website); + row.appendChild(hbox); + + richlistitem.appendChild(row); + return richlistitem; + }, + + _sortExceptions(list, frag, column) { + let sortDirection; + + if (!column) { + column = document.querySelector("treecol[data-isCurrentSortCol=true]"); + sortDirection = + column.getAttribute("data-last-sortDirection") || "ascending"; + } else { + sortDirection = column.getAttribute("data-last-sortDirection"); + sortDirection = + sortDirection === "ascending" ? "descending" : "ascending"; + } + + let sortFunc = (a, b) => { + return comp.compare(a.getAttribute("domain"), b.getAttribute("domain")); + }; + + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + let items = Array.from(frag.querySelectorAll("richlistitem")); + + if (sortDirection === "descending") { + items.sort((a, b) => sortFunc(b, a)); + } else { + items.sort(sortFunc); + } + + // Re-append items in the correct order: + items.forEach(item => frag.appendChild(item)); + + let cols = list.previousElementSibling.querySelectorAll("treecol"); + cols.forEach(c => { + c.removeAttribute("data-isCurrentSortCol"); + c.removeAttribute("sortDirection"); + }); + column.setAttribute("data-isCurrentSortCol", "true"); + column.setAttribute("sortDirection", sortDirection); + column.setAttribute("data-last-sortDirection", sortDirection); + }, + + _setRemoveButtonState() { + if (!this._list) { + return; + } + + if (this._prefLocked) { + this._removeAllButton.disabled = true; + this._removeButton.disabled = true; + return; + } + + let hasSelection = this._list.selectedIndex >= 0; + + this._removeButton.disabled = !hasSelection; + let disabledItems = this._list.querySelectorAll( + "label.website-name-value[disabled='true']" + ); + + this._removeAllButton.disabled = + this._list.itemCount == disabledItems.length; + }, + + onApplyChanges() { + if (this._exceptions.size == 0) { + Services.prefs.setStringPref("network.trr.excluded-domains", ""); + return; + } + + let exceptions = Array.from(this._exceptions); + let exceptionPrefString = exceptions.join(","); + + Services.prefs.setStringPref( + "network.trr.excluded-domains", + exceptionPrefString + ); + }, + + buildExceptionList(sortCol) { + // Clear old entries. + let oldItems = this._list.querySelectorAll("richlistitem"); + for (let item of oldItems) { + item.remove(); + } + let frag = document.createDocumentFragment(); + + let exceptions = Array.from(this._exceptions.values()); + + for (let exception of exceptions) { + let richlistitem = this._createExceptionListItem(exception); + frag.appendChild(richlistitem); + } + + // Sort exceptions. + this._sortExceptions(this._list, frag, sortCol); + + this._list.appendChild(frag); + + this._setRemoveButtonState(); + }, +}; + +document.addEventListener("DOMContentLoaded", () => { + gDoHExceptionsManager.init(); +}); diff --git a/browser/components/preferences/dialogs/dohExceptions.xhtml b/browser/components/preferences/dialogs/dohExceptions.xhtml new file mode 100644 index 0000000000..617e9fbfb0 --- /dev/null +++ b/browser/components/preferences/dialogs/dohExceptions.xhtml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window + id="DoHExceptionsDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="permissions-exceptions-doh-window" + data-l10n-attrs="title, style" + persist="width height" +> + <dialog + id="exceptionDialog" + buttons="accept,cancel" + data-l10n-id="permission-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="browser/preferences/permissions.ftl" + /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/dohExceptions.js" /> + + <keyset> + <key + data-l10n-id="permissions-close-key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane"> + <description + id="dohExceptionText" + control="url" + data-l10n-id="permissions-exceptions-manage-doh-desc" + /> + <separator class="thin" /> + <label + id="urlLabel" + control="url" + data-l10n-id="permissions-doh-entry-field" + /> + <hbox align="start"> + <html:input + id="url" + type="text" + style="flex: 1" + oninput="gDoHExceptionsManager.onExceptionInput();" + onkeypress="gDoHExceptionsManager.onExceptionKeyPress(event);" + /> + </hbox> + <hbox pack="end"> + <button + id="btnAddException" + disabled="true" + data-l10n-id="permissions-doh-add-exception" + oncommand="gDoHExceptionsManager.addException();" + /> + </hbox> + <separator class="thin" /> + <listheader> + <treecol + id="siteCol" + data-l10n-id="permissions-doh-col" + style="flex: 3 3 auto; width: 0" + data-isCurrentSortCol="true" + onclick="gDoHExceptionsManager.buildExceptionList(event.target)" + /> + </listheader> + <richlistbox + id="permissionsBox" + selected="false" + onkeypress="gDoHExceptionsManager.onListBoxKeyPress(event);" + onselect="gDoHExceptionsManager.onListBoxSelect();" + /> + </vbox> + + <hbox class="actionButtons"> + <button + id="removeException" + disabled="true" + data-l10n-id="permissions-doh-remove" + oncommand="gDoHExceptionsManager.onExceptionDelete();" + /> + <button + id="removeAllExceptions" + data-l10n-id="permissions-doh-remove-all" + oncommand="gDoHExceptionsManager.onAllExceptionsDelete();" + /> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/fonts.js b/browser/components/preferences/dialogs/fonts.js new file mode 100644 index 0000000000..ccc2f8faca --- /dev/null +++ b/browser/components/preferences/dialogs/fonts.js @@ -0,0 +1,173 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /browser/base/content/utilityOverlay.js */ +/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */ + +// browser.display.languageList LOCK ALL when LOCKED + +const kDefaultFontType = "font.default.%LANG%"; +const kFontNameFmtSerif = "font.name.serif.%LANG%"; +const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%"; +const kFontNameFmtMonospace = "font.name.monospace.%LANG%"; +const kFontNameListFmtSerif = "font.name-list.serif.%LANG%"; +const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%"; +const kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%"; +const kFontSizeFmtVariable = "font.size.variable.%LANG%"; +const kFontSizeFmtFixed = "font.size.monospace.%LANG%"; +const kFontMinSizeFmt = "font.minimum-size.%LANG%"; + +document + .getElementById("FontsDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); +window.addEventListener("load", () => gFontsDialog.onLoad()); + +Preferences.addAll([ + { id: "font.language.group", type: "wstring" }, + { id: "browser.display.use_document_fonts", type: "int" }, +]); + +var gFontsDialog = { + _selectLanguageGroupPromise: Promise.resolve(), + + onLoad() { + Preferences.addSyncFromPrefListener( + document.getElementById("selectLangs"), + () => this.readFontLanguageGroup() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("useDocumentFonts"), + () => this.readUseDocumentFonts() + ); + Preferences.addSyncToPrefListener( + document.getElementById("useDocumentFonts"), + () => this.writeUseDocumentFonts() + ); + for (let id of ["serif", "sans-serif", "monospace"]) { + let el = document.getElementById(id); + Preferences.addSyncFromPrefListener(el, () => + FontBuilder.readFontSelection(el) + ); + } + }, + + _selectLanguageGroup(aLanguageGroup) { + this._selectLanguageGroupPromise = (async () => { + // Avoid overlapping language group selections by awaiting the resolution + // of the previous one. We do this because this function is re-entrant, + // as inserting <preference> elements into the DOM sometimes triggers a call + // back into this function. And since this function is also asynchronous, + // that call can enter this function before the previous run has completed, + // which would corrupt the font menulists. Awaiting the previous call's + // resolution avoids that fate. + await this._selectLanguageGroupPromise; + + var prefs = [ + { + format: kDefaultFontType, + type: "string", + element: "defaultFontType", + fonttype: null, + }, + { + format: kFontNameFmtSerif, + type: "fontname", + element: "serif", + fonttype: "serif", + }, + { + format: kFontNameFmtSansSerif, + type: "fontname", + element: "sans-serif", + fonttype: "sans-serif", + }, + { + format: kFontNameFmtMonospace, + type: "fontname", + element: "monospace", + fonttype: "monospace", + }, + { + format: kFontNameListFmtSerif, + type: "unichar", + element: null, + fonttype: "serif", + }, + { + format: kFontNameListFmtSansSerif, + type: "unichar", + element: null, + fonttype: "sans-serif", + }, + { + format: kFontNameListFmtMonospace, + type: "unichar", + element: null, + fonttype: "monospace", + }, + { + format: kFontSizeFmtVariable, + type: "int", + element: "sizeVar", + fonttype: null, + }, + { + format: kFontSizeFmtFixed, + type: "int", + element: "sizeMono", + fonttype: null, + }, + { + format: kFontMinSizeFmt, + type: "int", + element: "minSize", + fonttype: null, + }, + ]; + for (var i = 0; i < prefs.length; ++i) { + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + var preference = Preferences.get(name); + if (!preference) { + preference = Preferences.add({ id: name, type: prefs[i].type }); + } + + if (!prefs[i].element) { + continue; + } + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) { + await FontBuilder.buildFontList( + aLanguageGroup, + prefs[i].fonttype, + element + ); + } + + preference.setElementValue(element); + } + } + })().catch(console.error); + }, + + readFontLanguageGroup() { + var languagePref = Preferences.get("font.language.group"); + this._selectLanguageGroup(languagePref.value); + return undefined; + }, + + readUseDocumentFonts() { + var preference = Preferences.get("browser.display.use_document_fonts"); + return preference.value == 1; + }, + + writeUseDocumentFonts() { + var useDocumentFonts = document.getElementById("useDocumentFonts"); + return useDocumentFonts.checked ? 1 : 0; + }, +}; diff --git a/browser/components/preferences/dialogs/fonts.xhtml b/browser/components/preferences/dialogs/fonts.xhtml new file mode 100644 index 0000000000..b5c9266384 --- /dev/null +++ b/browser/components/preferences/dialogs/fonts.xhtml @@ -0,0 +1,251 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="fonts-window" + data-l10n-attrs="title" + persist="lastSelected" +> + <dialog + id="FontsDialog" + buttons="accept,cancel,help" + helpTopic="prefs-fonts-and-colors" + > + <linkset> + <html:link rel="localization" href="browser/preferences/fonts.ftl" /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + + <keyset> + <key + data-l10n-id="fonts-window-close" + modifiers="accel" + oncommand="Preferences.close(event)" + /> + </keyset> + + <!-- Fonts for: [ Language ] --> + <groupbox> + <hbox align="center"> + <label control="selectLangs" + ><html:h2 data-l10n-id="fonts-langgroup-header" + /></label> + </hbox> + <menulist id="selectLangs" preference="font.language.group"> + <menupopup> + <menuitem value="ar" data-l10n-id="fonts-langgroup-arabic" /> + <menuitem value="x-armn" data-l10n-id="fonts-langgroup-armenian" /> + <menuitem value="x-beng" data-l10n-id="fonts-langgroup-bengali" /> + <menuitem + value="zh-CN" + data-l10n-id="fonts-langgroup-simpl-chinese" + /> + <menuitem + value="zh-HK" + data-l10n-id="fonts-langgroup-trad-chinese-hk" + /> + <menuitem value="zh-TW" data-l10n-id="fonts-langgroup-trad-chinese" /> + <menuitem + value="x-cyrillic" + data-l10n-id="fonts-langgroup-cyrillic" + /> + <menuitem + value="x-devanagari" + data-l10n-id="fonts-langgroup-devanagari" + /> + <menuitem value="x-ethi" data-l10n-id="fonts-langgroup-ethiopic" /> + <menuitem value="x-geor" data-l10n-id="fonts-langgroup-georgian" /> + <menuitem value="el" data-l10n-id="fonts-langgroup-el" /> + <menuitem value="x-gujr" data-l10n-id="fonts-langgroup-gujarati" /> + <menuitem value="x-guru" data-l10n-id="fonts-langgroup-gurmukhi" /> + <menuitem value="he" data-l10n-id="fonts-langgroup-hebrew" /> + <menuitem value="ja" data-l10n-id="fonts-langgroup-japanese" /> + <menuitem value="x-knda" data-l10n-id="fonts-langgroup-kannada" /> + <menuitem value="x-khmr" data-l10n-id="fonts-langgroup-khmer" /> + <menuitem value="ko" data-l10n-id="fonts-langgroup-korean" /> + <menuitem value="x-western" data-l10n-id="fonts-langgroup-latin" /> + <menuitem value="x-mlym" data-l10n-id="fonts-langgroup-malayalam" /> + <menuitem value="x-math" data-l10n-id="fonts-langgroup-math" /> + <menuitem value="x-orya" data-l10n-id="fonts-langgroup-odia" /> + <menuitem value="x-sinh" data-l10n-id="fonts-langgroup-sinhala" /> + <menuitem value="x-tamil" data-l10n-id="fonts-langgroup-tamil" /> + <menuitem value="x-telu" data-l10n-id="fonts-langgroup-telugu" /> + <menuitem value="th" data-l10n-id="fonts-langgroup-thai" /> + <menuitem value="x-tibt" data-l10n-id="fonts-langgroup-tibetan" /> + <menuitem value="x-cans" data-l10n-id="fonts-langgroup-canadian" /> + <menuitem value="x-unicode" data-l10n-id="fonts-langgroup-other" /> + </menupopup> + </menulist> + + <separator class="thin" /> + + <box id="font-chooser-group"> + <!-- proportional row --> + <hbox align="center" pack="end"> + <label + data-l10n-id="fonts-proportional-header" + control="defaultFontType" + /> + </hbox> + <menulist id="defaultFontType"> + <menupopup> + <menuitem value="serif" data-l10n-id="fonts-default-serif" /> + <menuitem + value="sans-serif" + data-l10n-id="fonts-default-sans-serif" + /> + </menupopup> + </menulist> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-proportional-size" control="sizeVar" /> + </hbox> + <menulist id="sizeVar" delayprefsave="true"> + <menupopup> + <menuitem value="9" label="9" /> + <menuitem value="10" label="10" /> + <menuitem value="11" label="11" /> + <menuitem value="12" label="12" /> + <menuitem value="13" label="13" /> + <menuitem value="14" label="14" /> + <menuitem value="15" label="15" /> + <menuitem value="16" label="16" /> + <menuitem value="17" label="17" /> + <menuitem value="18" label="18" /> + <menuitem value="20" label="20" /> + <menuitem value="22" label="22" /> + <menuitem value="24" label="24" /> + <menuitem value="26" label="26" /> + <menuitem value="28" label="28" /> + <menuitem value="30" label="30" /> + <menuitem value="32" label="32" /> + <menuitem value="34" label="34" /> + <menuitem value="36" label="36" /> + <menuitem value="40" label="40" /> + <menuitem value="44" label="44" /> + <menuitem value="48" label="48" /> + <menuitem value="56" label="56" /> + <menuitem value="64" label="64" /> + <menuitem value="72" label="72" /> + </menupopup> + </menulist> + + <!-- serif row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-serif" control="serif" /> + </hbox> + <menulist id="serif" delayprefsave="true" /> + <spacer /> + <spacer /> + + <!-- sans-serif row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-sans-serif" control="sans-serif" /> + </hbox> + <menulist id="sans-serif" delayprefsave="true" /> + <spacer /> + <spacer /> + + <!-- monospace row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-monospace" control="monospace" /> + </hbox> + <!-- + FIXME(emilio): Why is this the only menulist here with crop="end"? + This goes back to the beginning of time... + --> + <menulist id="monospace" crop="end" delayprefsave="true" /> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-monospace-size" control="sizeMono" /> + </hbox> + <menulist id="sizeMono" delayprefsave="true"> + <menupopup> + <menuitem value="9" label="9" /> + <menuitem value="10" label="10" /> + <menuitem value="11" label="11" /> + <menuitem value="12" label="12" /> + <menuitem value="13" label="13" /> + <menuitem value="14" label="14" /> + <menuitem value="15" label="15" /> + <menuitem value="16" label="16" /> + <menuitem value="17" label="17" /> + <menuitem value="18" label="18" /> + <menuitem value="20" label="20" /> + <menuitem value="22" label="22" /> + <menuitem value="24" label="24" /> + <menuitem value="26" label="26" /> + <menuitem value="28" label="28" /> + <menuitem value="30" label="30" /> + <menuitem value="32" label="32" /> + <menuitem value="34" label="34" /> + <menuitem value="36" label="36" /> + <menuitem value="40" label="40" /> + <menuitem value="44" label="44" /> + <menuitem value="48" label="48" /> + <menuitem value="56" label="56" /> + <menuitem value="64" label="64" /> + <menuitem value="72" label="72" /> + </menupopup> + </menulist> + </box> + <separator class="thin" /> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-minsize" control="minSize" /> + <menulist id="minSize"> + <menupopup> + <menuitem value="0" data-l10n-id="fonts-minsize-none" /> + <menuitem value="9" label="9" /> + <menuitem value="10" label="10" /> + <menuitem value="11" label="11" /> + <menuitem value="12" label="12" /> + <menuitem value="13" label="13" /> + <menuitem value="14" label="14" /> + <menuitem value="15" label="15" /> + <menuitem value="16" label="16" /> + <menuitem value="17" label="17" /> + <menuitem value="18" label="18" /> + <menuitem value="20" label="20" /> + <menuitem value="22" label="22" /> + <menuitem value="24" label="24" /> + <menuitem value="26" label="26" /> + <menuitem value="28" label="28" /> + <menuitem value="30" label="30" /> + <menuitem value="32" label="32" /> + <menuitem value="34" label="34" /> + <menuitem value="36" label="36" /> + <menuitem value="40" label="40" /> + <menuitem value="44" label="44" /> + <menuitem value="48" label="48" /> + <menuitem value="56" label="56" /> + <menuitem value="64" label="64" /> + <menuitem value="72" label="72" /> + </menupopup> + </menulist> + </hbox> + <separator /> + <separator class="groove" /> + <hbox> + <checkbox + id="useDocumentFonts" + data-l10n-id="fonts-allow-own" + preference="browser.display.use_document_fonts" + /> + </hbox> + </groupbox> + + <!-- Load the script after the elements for layout issues (bug 1501755). --> + <script src="chrome://mozapps/content/preferences/fontbuilder.js" /> + <script src="chrome://browser/content/preferences/dialogs/fonts.js" /> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/handlers.css b/browser/components/preferences/dialogs/handlers.css new file mode 100644 index 0000000000..b53963fa4d --- /dev/null +++ b/browser/components/preferences/dialogs/handlers.css @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Make the icons appear. + * Note: we display the icon box for every item whether or not it has an icon + * so the labels of all the items align vertically. + */ +.actionsMenu > menupopup > menuitem > .menu-iconic-left { + display: flex; + min-width: 16px; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + #handlersView > richlistitem, + .actionsMenu > menupopup > menuitem > .menu-iconic-left { + image-rendering: -moz-crisp-edges; + } +} diff --git a/browser/components/preferences/dialogs/jar.mn b/browser/components/preferences/dialogs/jar.mn new file mode 100644 index 0000000000..a9f3dd1da2 --- /dev/null +++ b/browser/components/preferences/dialogs/jar.mn @@ -0,0 +1,49 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/preferences/dialogs/addEngine.xhtml + content/browser/preferences/dialogs/addEngine.js + content/browser/preferences/dialogs/addEngine.css + content/browser/preferences/dialogs/applicationManager.xhtml + content/browser/preferences/dialogs/applicationManager.js + content/browser/preferences/dialogs/blocklists.xhtml + content/browser/preferences/dialogs/blocklists.js + content/browser/preferences/dialogs/browserLanguages.xhtml + content/browser/preferences/dialogs/browserLanguages.js + content/browser/preferences/dialogs/clearSiteData.css + content/browser/preferences/dialogs/clearSiteData.js + content/browser/preferences/dialogs/clearSiteData.xhtml + content/browser/preferences/dialogs/colors.xhtml + content/browser/preferences/dialogs/colors.js + content/browser/preferences/dialogs/connection.xhtml + content/browser/preferences/dialogs/connection.js + content/browser/preferences/dialogs/dohExceptions.xhtml + content/browser/preferences/dialogs/dohExceptions.js + content/browser/preferences/dialogs/fonts.xhtml + content/browser/preferences/dialogs/fonts.js + content/browser/preferences/dialogs/handlers.css + content/browser/preferences/dialogs/languages.xhtml + content/browser/preferences/dialogs/languages.js + content/browser/preferences/dialogs/permissions.xhtml + content/browser/preferences/dialogs/sitePermissions.xhtml + content/browser/preferences/dialogs/sitePermissions.js + content/browser/preferences/dialogs/sitePermissions.css + content/browser/preferences/dialogs/containers.xhtml + content/browser/preferences/dialogs/containers.js + content/browser/preferences/dialogs/permissions.js + content/browser/preferences/dialogs/sanitize.xhtml + content/browser/preferences/dialogs/sanitize.js + content/browser/preferences/dialogs/selectBookmark.xhtml + content/browser/preferences/dialogs/selectBookmark.js + content/browser/preferences/dialogs/siteDataSettings.xhtml + content/browser/preferences/dialogs/siteDataSettings.js +* content/browser/preferences/dialogs/siteDataRemoveSelected.xhtml + content/browser/preferences/dialogs/siteDataRemoveSelected.js + content/browser/preferences/dialogs/syncChooseWhatToSync.xhtml + content/browser/preferences/dialogs/syncChooseWhatToSync.js + content/browser/preferences/dialogs/translationExceptions.xhtml + content/browser/preferences/dialogs/translationExceptions.js + content/browser/preferences/dialogs/translations.xhtml + content/browser/preferences/dialogs/translations.js diff --git a/browser/components/preferences/dialogs/languages.js b/browser/components/preferences/dialogs/languages.js new file mode 100644 index 0000000000..502a083c8b --- /dev/null +++ b/browser/components/preferences/dialogs/languages.js @@ -0,0 +1,384 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +document + .getElementById("LanguagesDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); + +Preferences.addAll([ + { id: "intl.accept_languages", type: "wstring" }, + { id: "pref.browser.language.disable_button.up", type: "bool" }, + { id: "pref.browser.language.disable_button.down", type: "bool" }, + { id: "pref.browser.language.disable_button.remove", type: "bool" }, + { id: "privacy.spoof_english", type: "int" }, +]); + +var gLanguagesDialog = { + _availableLanguagesList: [], + _acceptLanguages: {}, + + _selectedItemID: null, + + onLoad() { + let spoofEnglishElement = document.getElementById("spoofEnglish"); + Preferences.addSyncFromPrefListener(spoofEnglishElement, () => + gLanguagesDialog.readSpoofEnglish() + ); + Preferences.addSyncToPrefListener(spoofEnglishElement, () => + gLanguagesDialog.writeSpoofEnglish() + ); + + Preferences.get("intl.accept_languages").on("change", () => + this._readAcceptLanguages().catch(console.error) + ); + + if (!this._availableLanguagesList.length) { + document.mozSubdialogReady = this._loadAvailableLanguages(); + } + }, + + get _activeLanguages() { + return document.getElementById("activeLanguages"); + }, + + get _availableLanguages() { + return document.getElementById("availableLanguages"); + }, + + async _loadAvailableLanguages() { + // This is a parser for: resource://gre/res/language.properties + // The file is formatted like so: + // ab[-cd].accept=true|false + // ab = language + // cd = region + var bundleAccepted = document.getElementById("bundleAccepted"); + + function LocaleInfo(aLocaleName, aLocaleCode, aIsVisible) { + this.name = aLocaleName; + this.code = aLocaleCode; + this.isVisible = aIsVisible; + } + + // 1) Read the available languages out of language.properties + + let localeCodes = []; + let localeValues = []; + for (let currString of bundleAccepted.strings) { + var property = currString.key.split("."); // ab[-cd].accept + if (property[1] == "accept") { + localeCodes.push(property[0]); + localeValues.push(currString.value); + } + } + + let localeNames = Services.intl.getLocaleDisplayNames( + undefined, + localeCodes + ); + + for (let i in localeCodes) { + let isVisible = + localeValues[i] == "true" && + (!(localeCodes[i] in this._acceptLanguages) || + !this._acceptLanguages[localeCodes[i]]); + + let li = new LocaleInfo(localeNames[i], localeCodes[i], isVisible); + this._availableLanguagesList.push(li); + } + + await this._buildAvailableLanguageList(); + await this._readAcceptLanguages(); + }, + + async _buildAvailableLanguageList() { + var availableLanguagesPopup = document.getElementById( + "availableLanguagesPopup" + ); + while (availableLanguagesPopup.hasChildNodes()) { + availableLanguagesPopup.firstChild.remove(); + } + + let frag = document.createDocumentFragment(); + + // Load the UI with the data + for (var i = 0; i < this._availableLanguagesList.length; ++i) { + let locale = this._availableLanguagesList[i]; + let localeCode = locale.code; + if ( + locale.isVisible && + (!(localeCode in this._acceptLanguages) || + !this._acceptLanguages[localeCode]) + ) { + var menuitem = document.createXULElement("menuitem"); + menuitem.id = localeCode; + document.l10n.setAttributes(menuitem, "languages-code-format", { + locale: locale.name, + code: localeCode, + }); + frag.appendChild(menuitem); + } + } + + await document.l10n.translateFragment(frag); + + // Sort the list of languages by name + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + let items = Array.from(frag.children); + + items.sort((a, b) => { + return comp.compare(a.getAttribute("label"), b.getAttribute("label")); + }); + + // Re-append items in the correct order: + items.forEach(item => frag.appendChild(item)); + + availableLanguagesPopup.appendChild(frag); + + this._availableLanguages.setAttribute( + "label", + this._availableLanguages.getAttribute("placeholder") + ); + }, + + async _readAcceptLanguages() { + while (this._activeLanguages.hasChildNodes()) { + this._activeLanguages.firstChild.remove(); + } + + var selectedIndex = 0; + var preference = Preferences.get("intl.accept_languages"); + if (preference.value == "") { + return; + } + var languages = preference.value.toLowerCase().split(/\s*,\s*/); + for (var i = 0; i < languages.length; ++i) { + var listitem = document.createXULElement("richlistitem"); + var label = document.createXULElement("label"); + listitem.appendChild(label); + listitem.id = languages[i]; + if (languages[i] == this._selectedItemID) { + selectedIndex = i; + } + this._activeLanguages.appendChild(listitem); + var localeName = this._getLocaleName(languages[i]); + document.l10n.setAttributes(label, "languages-active-code-format", { + locale: localeName, + code: languages[i], + }); + + // Hash this language as an "Active" language so we don't + // show it in the list that can be added. + this._acceptLanguages[languages[i]] = true; + } + + // We're forcing an early localization here because otherwise + // the initial sizing of the dialog will happen before it and + // result in overflow. + await document.l10n.translateFragment(this._activeLanguages); + + if (this._activeLanguages.childNodes.length) { + this._activeLanguages.ensureIndexIsVisible(selectedIndex); + this._activeLanguages.selectedIndex = selectedIndex; + } + + // Update states of accept-language list and buttons according to + // privacy.resistFingerprinting and privacy.spoof_english. + this.readSpoofEnglish(); + }, + + onAvailableLanguageSelect() { + var availableLanguages = this._availableLanguages; + var addButton = document.getElementById("addButton"); + addButton.disabled = + availableLanguages.disabled || availableLanguages.selectedIndex < 0; + + this._availableLanguages.removeAttribute("accesskey"); + }, + + addLanguage() { + var selectedID = this._availableLanguages.selectedItem.id; + var preference = Preferences.get("intl.accept_languages"); + var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/); + for (var i = 0; i < arrayOfPrefs.length; ++i) { + if (arrayOfPrefs[i] == selectedID) { + return; + } + } + + this._selectedItemID = selectedID; + + if (preference.value == "") { + preference.value = selectedID; + } else { + arrayOfPrefs.unshift(selectedID); + preference.value = arrayOfPrefs.join(","); + } + + this._acceptLanguages[selectedID] = true; + this._availableLanguages.selectedItem = null; + + // Rebuild the available list with the added item removed... + this._buildAvailableLanguageList().catch(console.error); + }, + + removeLanguage() { + // Build the new preference value string. + var languagesArray = []; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + if (!item.selected) { + languagesArray.push(item.id); + } else { + this._acceptLanguages[item.id] = false; + } + } + var string = languagesArray.join(","); + + // Get the item to select after the remove operation completes. + var selection = this._activeLanguages.selectedItems; + var lastSelected = selection[selection.length - 1]; + var selectItem = lastSelected.nextSibling || lastSelected.previousSibling; + selectItem = selectItem ? selectItem.id : null; + + this._selectedItemID = selectItem; + + // Update the preference and force a UI rebuild + var preference = Preferences.get("intl.accept_languages"); + preference.value = string; + + this._buildAvailableLanguageList().catch(console.error); + }, + + _getLocaleName(localeCode) { + if (!this._availableLanguagesList.length) { + this._loadAvailableLanguages(); + } + let languageName = ""; + for (var i = 0; i < this._availableLanguagesList.length; ++i) { + if (localeCode == this._availableLanguagesList[i].code) { + return this._availableLanguagesList[i].name; + } + // Try resolving the locale code without region code. Can't return + // directly because there might be a perfect match later. + if (localeCode.split("-")[0] == this._availableLanguagesList[i].code) { + languageName = this._availableLanguagesList[i].name; + } + } + + return languageName; + }, + + moveUp() { + var selectedItem = this._activeLanguages.selectedItems[0]; + var previousItem = selectedItem.previousSibling; + + var string = ""; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + string += i == 0 ? "" : ","; + if (item.id == previousItem.id) { + string += selectedItem.id; + } else if (item.id == selectedItem.id) { + string += previousItem.id; + } else { + string += item.id; + } + } + + this._selectedItemID = selectedItem.id; + + // Update the preference and force a UI rebuild + var preference = Preferences.get("intl.accept_languages"); + preference.value = string; + }, + + moveDown() { + var selectedItem = this._activeLanguages.selectedItems[0]; + var nextItem = selectedItem.nextSibling; + + var string = ""; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + string += i == 0 ? "" : ","; + if (item.id == nextItem.id) { + string += selectedItem.id; + } else if (item.id == selectedItem.id) { + string += nextItem.id; + } else { + string += item.id; + } + } + + this._selectedItemID = selectedItem.id; + + // Update the preference and force a UI rebuild + var preference = Preferences.get("intl.accept_languages"); + preference.value = string; + }, + + onLanguageSelect() { + var upButton = document.getElementById("up"); + var downButton = document.getElementById("down"); + var removeButton = document.getElementById("remove"); + switch (this._activeLanguages.selectedCount) { + case 0: + upButton.disabled = downButton.disabled = removeButton.disabled = true; + break; + case 1: + upButton.disabled = this._activeLanguages.selectedIndex == 0; + downButton.disabled = + this._activeLanguages.selectedIndex == + this._activeLanguages.childNodes.length - 1; + removeButton.disabled = false; + break; + default: + upButton.disabled = true; + downButton.disabled = true; + removeButton.disabled = false; + } + }, + + readSpoofEnglish() { + var checkbox = document.getElementById("spoofEnglish"); + var resistFingerprinting = Services.prefs.getBoolPref( + "privacy.resistFingerprinting" + ); + if (!resistFingerprinting) { + checkbox.hidden = true; + return false; + } + + var spoofEnglish = Preferences.get("privacy.spoof_english").value; + var activeLanguages = this._activeLanguages; + var availableLanguages = this._availableLanguages; + checkbox.hidden = false; + switch (spoofEnglish) { + case 1: // don't spoof intl.accept_languages + activeLanguages.disabled = false; + activeLanguages.selectItem(activeLanguages.firstChild); + availableLanguages.disabled = false; + this.onAvailableLanguageSelect(); + return false; + case 2: // spoof intl.accept_languages + activeLanguages.clearSelection(); + activeLanguages.disabled = true; + availableLanguages.disabled = true; + this.onAvailableLanguageSelect(); + return true; + default: + // will prompt for spoofing intl.accept_languages if resisting fingerprinting + return false; + } + }, + + writeSpoofEnglish() { + return document.getElementById("spoofEnglish").checked ? 2 : 1; + }, +}; diff --git a/browser/components/preferences/dialogs/languages.xhtml b/browser/components/preferences/dialogs/languages.xhtml new file mode 100644 index 0000000000..bd509e84e4 --- /dev/null +++ b/browser/components/preferences/dialogs/languages.xhtml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="webpage-languages-window2" + data-l10n-attrs="title, style" + persist="lastSelected" + onload="gLanguagesDialog.onLoad();" +> + <dialog + id="LanguagesDialog" + buttons="accept,cancel,help" + helpTopic="prefs-languages" + > + <linkset> + <html:link rel="localization" href="browser/preferences/languages.ftl" /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://browser/content/preferences/dialogs/languages.js" /> + + <keyset> + <key + data-l10n-id="languages-close-key" + modifiers="accel" + oncommand="Preferences.close(event)" + /> + </keyset> + + <stringbundleset id="languageSet"> + <stringbundle + id="bundleAccepted" + src="resource://gre/res/language.properties" + /> + </stringbundleset> + + <description data-l10n-id="languages-description" /> + <checkbox + id="spoofEnglish" + data-l10n-id="languages-customize-spoof-english" + preference="privacy.spoof_english" + /> + <box class="languages-grid"> + <richlistbox + id="activeLanguages" + seltype="multiple" + onselect="gLanguagesDialog.onLanguageSelect();" + /> + <vbox> + <button + id="up" + class="up" + oncommand="gLanguagesDialog.moveUp();" + disabled="true" + data-l10n-id="languages-customize-moveup" + preference="pref.browser.language.disable_button.up" + /> + <button + id="down" + class="down" + oncommand="gLanguagesDialog.moveDown();" + disabled="true" + data-l10n-id="languages-customize-movedown" + preference="pref.browser.language.disable_button.down" + /> + <button + id="remove" + oncommand="gLanguagesDialog.removeLanguage();" + disabled="true" + data-l10n-id="languages-customize-remove" + preference="pref.browser.language.disable_button.remove" + /> + </vbox> + <!-- This <vbox> is needed to position search tooltips correctly. --> + <vbox> + <menulist + id="availableLanguages" + oncommand="gLanguagesDialog.onAvailableLanguageSelect();" + data-l10n-id="languages-customize-select-language" + data-l10n-attrs="placeholder" + > + <menupopup id="availableLanguagesPopup" /> + </menulist> + </vbox> + <button + id="addButton" + class="add-web-language" + oncommand="gLanguagesDialog.addLanguage();" + disabled="true" + data-l10n-id="languages-customize-add" + /> + </box> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/moz.build b/browser/components/preferences/dialogs/moz.build new file mode 100644 index 0000000000..603c560505 --- /dev/null +++ b/browser/components/preferences/dialogs/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +for var in ("MOZ_APP_NAME", "MOZ_MACBUNDLE_NAME"): + DEFINES[var] = CONFIG[var] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"): + DEFINES["HAVE_SHELL_SERVICE"] = 1 + +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/components/preferences/dialogs/permissions.js b/browser/components/preferences/dialogs/permissions.js new file mode 100644 index 0000000000..3ca2664611 --- /dev/null +++ b/browser/components/preferences/dialogs/permissions.js @@ -0,0 +1,645 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "contentBlockingAllowList", + "@mozilla.org/content-blocking-allow-list;1", + "nsIContentBlockingAllowList" +); + +const permissionExceptionsL10n = { + trackingprotection: { + window: "permissions-exceptions-etp-window2", + description: "permissions-exceptions-manage-etp-desc", + }, + cookie: { + window: "permissions-exceptions-cookie-window2", + description: "permissions-exceptions-cookie-desc", + }, + popup: { + window: "permissions-exceptions-popup-window2", + description: "permissions-exceptions-popup-desc", + }, + "login-saving": { + window: "permissions-exceptions-saved-logins-window2", + description: "permissions-exceptions-saved-logins-desc", + }, + "https-only-load-insecure": { + window: "permissions-exceptions-https-only-window2", + description: "permissions-exceptions-https-only-desc", + }, + install: { + window: "permissions-exceptions-addons-window2", + description: "permissions-exceptions-addons-desc", + }, +}; + +function Permission(principal, type, capability) { + this.principal = principal; + this.origin = principal.origin; + this.type = type; + this.capability = capability; +} + +var gPermissionManager = { + _type: "", + _isObserving: false, + _permissions: new Map(), + _permissionsToAdd: new Map(), + _permissionsToDelete: new Map(), + _bundle: null, + _list: null, + _removeButton: null, + _removeAllButton: null, + + onLoad() { + let params = window.arguments[0]; + document.mozSubdialogReady = this.init(params); + }, + + async init(params) { + if (!this._isObserving) { + Services.obs.addObserver(this, "perm-changed"); + this._isObserving = true; + } + + document.addEventListener("dialogaccept", () => this.onApplyChanges()); + + this._type = params.permissionType; + this._list = document.getElementById("permissionsBox"); + this._removeButton = document.getElementById("removePermission"); + this._removeAllButton = document.getElementById("removeAllPermissions"); + + this._btnCookieSession = document.getElementById("btnCookieSession"); + this._btnBlock = document.getElementById("btnBlock"); + this._btnDisableETP = document.getElementById("btnDisableETP"); + this._btnAllow = document.getElementById("btnAllow"); + this._btnHttpsOnlyOff = document.getElementById("btnHttpsOnlyOff"); + this._btnHttpsOnlyOffTmp = document.getElementById("btnHttpsOnlyOffTmp"); + + let permissionsText = document.getElementById("permissionsText"); + + let l10n = permissionExceptionsL10n[this._type]; + document.l10n.setAttributes(permissionsText, l10n.description); + document.l10n.setAttributes(document.documentElement, l10n.window); + + let urlFieldVisible = + params.blockVisible || + params.sessionVisible || + params.allowVisible || + params.disableETPVisible; + + this._urlField = document.getElementById("url"); + this._urlField.value = params.prefilledHost; + this._urlField.hidden = !urlFieldVisible; + + await document.l10n.translateElements([ + permissionsText, + document.documentElement, + ]); + + document.getElementById("btnDisableETP").hidden = !params.disableETPVisible; + document.getElementById("btnBlock").hidden = !params.blockVisible; + document.getElementById("btnCookieSession").hidden = !( + params.sessionVisible && this._type == "cookie" + ); + document.getElementById("btnHttpsOnlyOff").hidden = !( + this._type == "https-only-load-insecure" + ); + document.getElementById("btnHttpsOnlyOffTmp").hidden = !( + params.sessionVisible && this._type == "https-only-load-insecure" + ); + document.getElementById("btnAllow").hidden = !params.allowVisible; + + this.onHostInput(this._urlField); + + let urlLabel = document.getElementById("urlLabel"); + urlLabel.hidden = !urlFieldVisible; + + this._hideStatusColumn = params.hideStatusColumn; + let statusCol = document.getElementById("statusCol"); + statusCol.hidden = this._hideStatusColumn; + if (this._hideStatusColumn) { + statusCol.removeAttribute("data-isCurrentSortCol"); + document + .getElementById("siteCol") + .setAttribute("data-isCurrentSortCol", "true"); + } + + Services.obs.notifyObservers(null, "flush-pending-permissions", this._type); + + this._loadPermissions(); + this.buildPermissionsList(); + + this._urlField.focus(); + }, + + uninit() { + if (this._isObserving) { + Services.obs.removeObserver(this, "perm-changed"); + this._isObserving = false; + } + }, + + observe(subject, topic, data) { + if (topic !== "perm-changed") { + return; + } + + let permission = subject.QueryInterface(Ci.nsIPermission); + + // Ignore unrelated permission types. + if (permission.type !== this._type) { + return; + } + + if (data == "added") { + this._addPermissionToList(permission); + this.buildPermissionsList(); + } else if (data == "changed") { + let p = this._permissions.get(permission.principal.origin); + // Maybe this item has been excluded before because it had an invalid capability. + if (p) { + p.capability = permission.capability; + this._handleCapabilityChange(p); + } else { + this._addPermissionToList(permission); + } + this.buildPermissionsList(); + } else if (data == "deleted") { + this._removePermissionFromList(permission.principal.origin); + } + }, + + _handleCapabilityChange(perm) { + let permissionlistitem = document.getElementsByAttribute( + "origin", + perm.origin + )[0]; + document.l10n.setAttributes( + permissionlistitem.querySelector(".website-capability-value"), + this._getCapabilityL10nId(perm.capability) + ); + }, + + _isCapabilitySupported(capability) { + return ( + capability == Ci.nsIPermissionManager.ALLOW_ACTION || + capability == Ci.nsIPermissionManager.DENY_ACTION || + capability == Ci.nsICookiePermission.ACCESS_SESSION || + // Bug 1753600 there are still a few legacy cookies around that have the capability 9, + // _getCapabilityL10nId will throw if it receives a capability of 9 + // that is not in combination with the type https-only-load-insecure + (capability == + Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION && + this._type == "https-only-load-insecure") + ); + }, + + _getCapabilityL10nId(capability) { + // HTTPS-Only Mode phrases exceptions as turning it off + if (this._type == "https-only-load-insecure") { + return this._getHttpsOnlyCapabilityL10nId(capability); + } + + switch (capability) { + case Ci.nsIPermissionManager.ALLOW_ACTION: + return "permissions-capabilities-listitem-allow"; + case Ci.nsIPermissionManager.DENY_ACTION: + return "permissions-capabilities-listitem-block"; + case Ci.nsICookiePermission.ACCESS_SESSION: + return "permissions-capabilities-listitem-allow-session"; + default: + throw new Error(`Unknown capability: ${capability}`); + } + }, + + _getHttpsOnlyCapabilityL10nId(capability) { + switch (capability) { + case Ci.nsIPermissionManager.ALLOW_ACTION: + return "permissions-capabilities-listitem-off"; + case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION: + return "permissions-capabilities-listitem-off-temporarily"; + default: + throw new Error(`Unknown HTTPS-Only Mode capability: ${capability}`); + } + }, + + _addPermissionToList(perm) { + if (perm.type !== this._type) { + return; + } + if (!this._isCapabilitySupported(perm.capability)) { + return; + } + + // Skip private browsing session permissions. + if ( + perm.principal.privateBrowsingId !== + Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID && + perm.expireType === Services.perms.EXPIRE_SESSION + ) { + return; + } + + let p = new Permission(perm.principal, perm.type, perm.capability); + this._permissions.set(p.origin, p); + }, + + _addOrModifyPermission(principal, capability) { + // check whether the permission already exists, if not, add it + let permissionParams = { principal, type: this._type, capability }; + let existingPermission = this._permissions.get(principal.origin); + if (!existingPermission) { + this._permissionsToAdd.set(principal.origin, permissionParams); + this._addPermissionToList(permissionParams); + this.buildPermissionsList(); + } else if (existingPermission.capability != capability) { + existingPermission.capability = capability; + this._permissionsToAdd.set(principal.origin, permissionParams); + this._handleCapabilityChange(existingPermission); + } + }, + + _addNewPrincipalToList(list, uri) { + list.push(Services.scriptSecurityManager.createContentPrincipal(uri, {})); + // If we have ended up with an unknown scheme, the following will throw. + list[list.length - 1].origin; + }, + + addPermission(capability) { + let textbox = document.getElementById("url"); + let input_url = textbox.value.trim(); // trim any leading and trailing space + let principals = []; + try { + // The origin accessor on the principal object will throw if the + // principal doesn't have a canonical origin representation. This will + // help catch cases where the URI parser parsed something like + // `localhost:8080` as having the scheme `localhost`, rather than being + // an invalid URI. A canonical origin representation is required by the + // permission manager for storage, so this won't prevent any valid + // permissions from being entered by the user. + try { + let uri = Services.io.newURI(input_url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + if (principal.origin.startsWith("moz-nullprincipal:")) { + throw new Error("Null principal"); + } + principals.push(principal); + } catch (ex) { + this._addNewPrincipalToList( + principals, + Services.io.newURI("http://" + input_url) + ); + this._addNewPrincipalToList( + principals, + Services.io.newURI("https://" + input_url) + ); + } + } catch (ex) { + document.l10n + .formatValues([ + { id: "permissions-invalid-uri-title" }, + { id: "permissions-invalid-uri-label" }, + ]) + .then(([title, message]) => { + Services.prompt.alert(window, title, message); + }); + return; + } + // In case of an ETP exception we compute the contentBlockingAllowList principal + // to align with the allow list behavior triggered by the protections panel + if (this._type == "trackingprotection") { + principals = principals.map( + lazy.contentBlockingAllowList.computeContentBlockingAllowListPrincipal + ); + } + for (let principal of principals) { + this._addOrModifyPermission(principal, capability); + } + + textbox.value = ""; + textbox.focus(); + + // covers a case where the site exists already, so the buttons don't disable + this.onHostInput(textbox); + + // enable "remove all" button as needed + this._setRemoveButtonState(); + }, + + _removePermission(permission) { + this._removePermissionFromList(permission.origin); + + // If this permission was added during this session, let's remove + // it from the pending adds list to prevent calls to the + // permission manager. + let isNewPermission = this._permissionsToAdd.delete(permission.origin); + if (!isNewPermission) { + this._permissionsToDelete.set(permission.origin, permission); + } + }, + + _removePermissionFromList(origin) { + this._permissions.delete(origin); + let permissionlistitem = document.getElementsByAttribute( + "origin", + origin + )[0]; + if (permissionlistitem) { + permissionlistitem.remove(); + } + }, + + _loadPermissions() { + // load permissions into a table. + for (let nextPermission of Services.perms.all) { + this._addPermissionToList(nextPermission); + } + }, + + _createPermissionListItem(permission) { + let disabledByPolicy = this._permissionDisabledByPolicy(permission); + let richlistitem = document.createXULElement("richlistitem"); + richlistitem.setAttribute("origin", permission.origin); + let row = document.createXULElement("hbox"); + row.setAttribute("style", "flex: 1"); + + let hbox = document.createXULElement("hbox"); + let website = document.createXULElement("label"); + website.setAttribute("disabled", disabledByPolicy); + website.setAttribute("class", "website-name-value"); + website.setAttribute("value", permission.origin); + hbox.setAttribute("class", "website-name"); + hbox.setAttribute("style", "flex: 3 3; width: 0"); + hbox.appendChild(website); + row.appendChild(hbox); + + if (!this._hideStatusColumn) { + hbox = document.createXULElement("hbox"); + let capability = document.createXULElement("label"); + capability.setAttribute("disabled", disabledByPolicy); + capability.setAttribute("class", "website-capability-value"); + document.l10n.setAttributes( + capability, + this._getCapabilityL10nId(permission.capability) + ); + hbox.setAttribute("class", "website-name"); + hbox.setAttribute("style", "flex: 1; width: 0"); + hbox.appendChild(capability); + row.appendChild(hbox); + } + + richlistitem.appendChild(row); + return richlistitem; + }, + + onWindowKeyPress(event) { + // Prevent dialog.js from closing the dialog when the user submits the input + // field via the return key. + if ( + event.keyCode == KeyEvent.DOM_VK_RETURN && + document.activeElement == this._urlField + ) { + event.preventDefault(); + } + }, + + onPermissionKeyPress(event) { + if (!this._list.selectedItem) { + return; + } + + if ( + event.keyCode == KeyEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + event.keyCode == KeyEvent.DOM_VK_BACK_SPACE) + ) { + this.onPermissionDelete(); + event.preventDefault(); + } + }, + + onHostKeyPress(event) { + if (event.keyCode == KeyEvent.DOM_VK_RETURN) { + if (!document.getElementById("btnAllow").hidden) { + document.getElementById("btnAllow").click(); + } else if (!document.getElementById("btnBlock").hidden) { + document.getElementById("btnBlock").click(); + } else if (!document.getElementById("btnHttpsOnlyOff").hidden) { + document.getElementById("btnHttpsOnlyOff").click(); + } else if (!document.getElementById("btnDisableETP").hidden) { + document.getElementById("btnDisableETP").click(); + } + } + }, + + onHostInput(siteField) { + this._btnCookieSession.disabled = + this._btnCookieSession.hidden || !siteField.value; + this._btnHttpsOnlyOff.disabled = + this._btnHttpsOnlyOff.hidden || !siteField.value; + this._btnHttpsOnlyOffTmp.disabled = + this._btnHttpsOnlyOffTmp.hidden || !siteField.value; + this._btnBlock.disabled = this._btnBlock.hidden || !siteField.value; + this._btnDisableETP.disabled = + this._btnDisableETP.hidden || !siteField.value; + this._btnAllow.disabled = this._btnAllow.hidden || !siteField.value; + }, + + _setRemoveButtonState() { + if (!this._list) { + return; + } + + let hasSelection = this._list.selectedIndex >= 0; + + let disabledByPolicy = false; + if (Services.policies.status === Services.policies.ACTIVE && hasSelection) { + let origin = this._list.selectedItem.getAttribute("origin"); + disabledByPolicy = this._permissionDisabledByPolicy( + this._permissions.get(origin) + ); + } + + this._removeButton.disabled = !hasSelection || disabledByPolicy; + let disabledItems = this._list.querySelectorAll( + "label.website-name-value[disabled='true']" + ); + + this._removeAllButton.disabled = + this._list.itemCount == disabledItems.length; + }, + + onPermissionDelete() { + let richlistitem = this._list.selectedItem; + let origin = richlistitem.getAttribute("origin"); + let permission = this._permissions.get(origin); + if (this._permissionDisabledByPolicy(permission)) { + return; + } + + this._removePermission(permission); + + this._setRemoveButtonState(); + }, + + onAllPermissionsDelete() { + for (let permission of this._permissions.values()) { + if (this._permissionDisabledByPolicy(permission)) { + continue; + } + this._removePermission(permission); + } + + this._setRemoveButtonState(); + }, + + onPermissionSelect() { + this._setRemoveButtonState(); + }, + + onApplyChanges() { + // Stop observing permission changes since we are about + // to write out the pending adds/deletes and don't need + // to update the UI + this.uninit(); + + for (let p of this._permissionsToDelete.values()) { + Services.perms.removeFromPrincipal(p.principal, p.type); + } + + for (let p of this._permissionsToAdd.values()) { + // If this sets the HTTPS-Only exemption only for this + // session, then the expire-type has to be set. + if ( + p.capability == + Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION + ) { + Services.perms.addFromPrincipal( + p.principal, + p.type, + p.capability, + Ci.nsIPermissionManager.EXPIRE_SESSION + ); + } else { + Services.perms.addFromPrincipal(p.principal, p.type, p.capability); + } + } + }, + + buildPermissionsList(sortCol) { + // Clear old entries. + let oldItems = this._list.querySelectorAll("richlistitem"); + for (let item of oldItems) { + item.remove(); + } + let frag = document.createDocumentFragment(); + + let permissions = Array.from(this._permissions.values()); + + for (let permission of permissions) { + let richlistitem = this._createPermissionListItem(permission); + frag.appendChild(richlistitem); + } + + // Sort permissions. + this._sortPermissions(this._list, frag, sortCol); + + this._list.appendChild(frag); + + this._setRemoveButtonState(); + }, + + _permissionDisabledByPolicy(permission) { + let permissionObject = Services.perms.getPermissionObject( + permission.principal, + this._type, + false + ); + return ( + permissionObject?.expireType == Ci.nsIPermissionManager.EXPIRE_POLICY + ); + }, + + _sortPermissions(list, frag, column) { + let sortDirection; + + if (!column) { + column = document.querySelector("treecol[data-isCurrentSortCol=true]"); + sortDirection = + column.getAttribute("data-last-sortDirection") || "ascending"; + } else { + sortDirection = column.getAttribute("data-last-sortDirection"); + sortDirection = + sortDirection === "ascending" ? "descending" : "ascending"; + } + + let sortFunc = null; + switch (column.id) { + case "siteCol": + sortFunc = (a, b) => { + return comp.compare( + a.getAttribute("origin"), + b.getAttribute("origin") + ); + }; + break; + + case "statusCol": + sortFunc = (a, b) => { + // The capabilities values ("Allow" and "Block") are localized asynchronously. + // Sort based on the guaranteed-present localization ID instead, note that the + // ascending/descending arrow may be pointing the wrong way. + return ( + a + .querySelector(".website-capability-value") + .getAttribute("data-l10n-id") > + b + .querySelector(".website-capability-value") + .getAttribute("data-l10n-id") + ); + }; + break; + } + + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + let items = Array.from(frag.querySelectorAll("richlistitem")); + + if (sortDirection === "descending") { + items.sort((a, b) => sortFunc(b, a)); + } else { + items.sort(sortFunc); + } + + // Re-append items in the correct order: + items.forEach(item => frag.appendChild(item)); + + let cols = list.previousElementSibling.querySelectorAll("treecol"); + cols.forEach(c => { + c.removeAttribute("data-isCurrentSortCol"); + c.removeAttribute("sortDirection"); + }); + column.setAttribute("data-isCurrentSortCol", "true"); + column.setAttribute("sortDirection", sortDirection); + column.setAttribute("data-last-sortDirection", sortDirection); + }, +}; diff --git a/browser/components/preferences/dialogs/permissions.xhtml b/browser/components/preferences/dialogs/permissions.xhtml new file mode 100644 index 0000000000..13d756bae0 --- /dev/null +++ b/browser/components/preferences/dialogs/permissions.xhtml @@ -0,0 +1,134 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window + id="PermissionsDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="permissions-window2" + data-l10n-attrs="title, style" + onload="gPermissionManager.onLoad();" + onunload="gPermissionManager.uninit();" + persist="width height" + onkeypress="gPermissionManager.onWindowKeyPress(event);" +> + <dialog + buttons="accept,cancel" + data-l10n-id="permission-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="browser/preferences/permissions.ftl" + /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/permissions.js" /> + + <keyset> + <key + data-l10n-id="permissions-close-key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane"> + <description id="permissionsText" control="url" /> + <separator class="thin" /> + <label id="urlLabel" control="url" data-l10n-id="permissions-address" /> + <hbox align="start"> + <html:input + id="url" + type="text" + style="flex: 1" + oninput="gPermissionManager.onHostInput(event.target);" + onkeypress="gPermissionManager.onHostKeyPress(event);" + /> + </hbox> + <hbox pack="end"> + <button + id="btnDisableETP" + disabled="true" + data-l10n-id="permissions-disable-etp" + oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);" + /> + <button + id="btnBlock" + disabled="true" + data-l10n-id="permissions-block" + oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);" + /> + <button + id="btnCookieSession" + disabled="true" + data-l10n-id="permissions-session" + oncommand="gPermissionManager.addPermission(Ci.nsICookiePermission.ACCESS_SESSION);" + /> + <button + id="btnAllow" + disabled="true" + data-l10n-id="permissions-allow" + oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);" + /> + <button + id="btnHttpsOnlyOff" + disabled="true" + data-l10n-id="permissions-button-off" + oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);" + /> + <button + id="btnHttpsOnlyOffTmp" + disabled="true" + data-l10n-id="permissions-button-off-temporarily" + oncommand="gPermissionManager.addPermission(Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION);" + /> + </hbox> + <separator class="thin" /> + <listheader> + <treecol + id="siteCol" + data-l10n-id="permissions-site-name" + style="flex: 3 3 auto; width: 0" + onclick="gPermissionManager.buildPermissionsList(event.target)" + /> + <treecol + id="statusCol" + data-l10n-id="permissions-status" + style="flex: 1 1 auto; width: 0" + data-isCurrentSortCol="true" + onclick="gPermissionManager.buildPermissionsList(event.target);" + /> + </listheader> + <richlistbox + id="permissionsBox" + selected="false" + onkeypress="gPermissionManager.onPermissionKeyPress(event);" + onselect="gPermissionManager.onPermissionSelect();" + /> + </vbox> + + <hbox class="actionButtons"> + <button + id="removePermission" + disabled="true" + data-l10n-id="permissions-remove" + oncommand="gPermissionManager.onPermissionDelete();" + /> + <button + id="removeAllPermissions" + data-l10n-id="permissions-remove-all" + oncommand="gPermissionManager.onAllPermissionsDelete();" + /> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/sanitize.js b/browser/components/preferences/dialogs/sanitize.js new file mode 100644 index 0000000000..37f818e011 --- /dev/null +++ b/browser/components/preferences/dialogs/sanitize.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +document + .querySelector("dialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); + +Preferences.addAll([ + { id: "privacy.clearOnShutdown.history", type: "bool" }, + { id: "privacy.clearOnShutdown.formdata", type: "bool" }, + { id: "privacy.clearOnShutdown.downloads", type: "bool" }, + { id: "privacy.clearOnShutdown.cookies", type: "bool" }, + { id: "privacy.clearOnShutdown.cache", type: "bool" }, + { id: "privacy.clearOnShutdown.offlineApps", type: "bool" }, + { id: "privacy.clearOnShutdown.sessions", type: "bool" }, + { id: "privacy.clearOnShutdown.siteSettings", type: "bool" }, +]); + +var gSanitizeDialog = Object.freeze({ + init() { + this.onClearHistoryChanged(); + + Preferences.get("privacy.clearOnShutdown.history").on( + "change", + this.onClearHistoryChanged.bind(this) + ); + }, + + onClearHistoryChanged() { + let downloadsPref = Preferences.get("privacy.clearOnShutdown.downloads"); + let historyPref = Preferences.get("privacy.clearOnShutdown.history"); + downloadsPref.value = historyPref.value; + }, +}); diff --git a/browser/components/preferences/dialogs/sanitize.xhtml b/browser/components/preferences/dialogs/sanitize.xhtml new file mode 100644 index 0000000000..40f030d8ab --- /dev/null +++ b/browser/components/preferences/dialogs/sanitize.xhtml @@ -0,0 +1,91 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<!DOCTYPE window> + +<window + id="SanitizeDialog" + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + persist="lastSelected" + data-l10n-id="sanitize-prefs2" + data-l10n-attrs="style" + onload="gSanitizeDialog.init();" +> + <dialog buttons="accept,cancel,help" helpTopic="prefs-clear-private-data"> + <linkset> + <html:link rel="localization" href="browser/sanitize.ftl" /> + <html:link rel="localization" href="branding/brand.ftl" /> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + + <keyset> + <key + data-l10n-id="window-close" + modifiers="accel" + oncommand="Preferences.close(event)" + /> + </keyset> + + <script src="chrome://browser/content/preferences/dialogs/sanitize.js" /> + + <description data-l10n-id="clear-data-settings-label"></description> + + <groupbox> + <label><html:h2 data-l10n-id="history-section-label" /></label> + <hbox> + <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style"> + <checkbox + data-l10n-id="item-history-and-downloads" + preference="privacy.clearOnShutdown.history" + /> + <checkbox + data-l10n-id="item-active-logins" + preference="privacy.clearOnShutdown.sessions" + /> + <checkbox + data-l10n-id="item-form-search-history" + preference="privacy.clearOnShutdown.formdata" + /> + </vbox> + <vbox> + <checkbox + data-l10n-id="item-cookies" + preference="privacy.clearOnShutdown.cookies" + /> + <checkbox + data-l10n-id="item-cache" + preference="privacy.clearOnShutdown.cache" + /> + </vbox> + </hbox> + </groupbox> + <groupbox> + <label><html:h2 data-l10n-id="data-section-label" /></label> + <hbox> + <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style"> + <checkbox + data-l10n-id="item-site-settings" + preference="privacy.clearOnShutdown.siteSettings" + /> + </vbox> + <vbox flex="1"> + <checkbox + data-l10n-id="item-offline-apps" + preference="privacy.clearOnShutdown.offlineApps" + /> + </vbox> + </hbox> + </groupbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/selectBookmark.js b/browser/components/preferences/dialogs/selectBookmark.js new file mode 100644 index 0000000000..5fe4c645e7 --- /dev/null +++ b/browser/components/preferences/dialogs/selectBookmark.js @@ -0,0 +1,119 @@ +//* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Shared Places Import - change other consumers if you change this: */ +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs", + PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyScriptGetter( + this, + "PlacesTreeView", + "chrome://browser/content/places/treeView.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"], + "chrome://browser/content/places/controller.js" +); +/* End Shared Places Import */ + +/** + * SelectBookmarkDialog controls the user interface for the "Use Bookmark for + * Home Page" dialog. + * + * The caller (gMainPane.setHomePageToBookmark in main.js) invokes this dialog + * with a single argument - a reference to an object with a .urls property and + * a .names property. This dialog is responsible for updating the contents of + * the .urls property with an array of URLs to use as home pages and for + * updating the .names property with an array of names for those URLs before it + * closes. + */ +var SelectBookmarkDialog = { + init: function SBD_init() { + document.getElementById("bookmarks").place = + "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY; + + // Initial update of the OK button. + this.selectionChanged(); + document.addEventListener("dialogaccept", function () { + SelectBookmarkDialog.accept(); + }); + }, + + /** + * Update the disabled state of the OK button as the user changes the + * selection within the view. + */ + selectionChanged: function SBD_selectionChanged() { + var accept = document + .getElementById("selectBookmarkDialog") + .getButton("accept"); + var bookmarks = document.getElementById("bookmarks"); + var disableAcceptButton = true; + if (bookmarks.hasSelection) { + if (!PlacesUtils.nodeIsSeparator(bookmarks.selectedNode)) { + disableAcceptButton = false; + } + } + accept.disabled = disableAcceptButton; + }, + + onItemDblClick: function SBD_onItemDblClick() { + var bookmarks = document.getElementById("bookmarks"); + var selectedNode = bookmarks.selectedNode; + if (selectedNode && PlacesUtils.nodeIsURI(selectedNode)) { + /** + * The user has double clicked on a tree row that is a link. Take this to + * mean that they want that link to be their homepage, and close the dialog. + */ + document + .getElementById("selectBookmarkDialog") + .getButton("accept") + .click(); + } + }, + + /** + * User accepts their selection. Set all the selected URLs or the contents + * of the selected folder as the list of homepages. + */ + accept: function SBD_accept() { + var bookmarks = document.getElementById("bookmarks"); + if (!bookmarks.hasSelection) { + throw new Error( + "Should not be able to accept dialog if there is no selected URL!" + ); + } + var urls = []; + var names = []; + var selectedNode = bookmarks.selectedNode; + if (PlacesUtils.nodeIsFolder(selectedNode)) { + let concreteGuid = PlacesUtils.getConcreteItemGuid(selectedNode); + var contents = PlacesUtils.getFolderContents(concreteGuid).root; + var cc = contents.childCount; + for (var i = 0; i < cc; ++i) { + var node = contents.getChild(i); + if (PlacesUtils.nodeIsURI(node)) { + urls.push(node.uri); + names.push(node.title); + } + } + contents.containerOpen = false; + } else { + urls.push(selectedNode.uri); + names.push(selectedNode.title); + } + window.arguments[0].urls = urls; + window.arguments[0].names = names; + }, +}; diff --git a/browser/components/preferences/dialogs/selectBookmark.xhtml b/browser/components/preferences/dialogs/selectBookmark.xhtml new file mode 100644 index 0000000000..3a46394328 --- /dev/null +++ b/browser/components/preferences/dialogs/selectBookmark.xhtml @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="select-bookmark-window2" + data-l10n-attrs="title, style" + persist="width height" + onload="SelectBookmarkDialog.init();" +> + <dialog id="selectBookmarkDialog"> + <linkset> + <html:link + rel="localization" + href="browser/preferences/selectBookmark.ftl" + /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/selectBookmark.js" /> + <script src="chrome://global/content/globalOverlay.js" /> + <script src="chrome://browser/content/utilityOverlay.js" /> + <script src="chrome://browser/content/places/places-tree.js" /> + + <description data-l10n-id="select-bookmark-desc" /> + + <separator class="thin" /> + + <tree + id="bookmarks" + flex="1" + is="places-tree" + style="height: 15em" + hidecolumnpicker="true" + seltype="single" + ondblclick="SelectBookmarkDialog.onItemDblClick();" + onselect="SelectBookmarkDialog.selectionChanged();" + disableUserActions="true" + > + <treecols> + <treecol id="title" flex="1" primary="true" hideheader="true" /> + </treecols> + <treechildren id="bookmarksChildren" flex="1" /> + </tree> + + <separator class="thin" /> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/siteDataRemoveSelected.js b/browser/components/preferences/dialogs/siteDataRemoveSelected.js new file mode 100644 index 0000000000..e722a2d826 --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * This dialog will ask the user to confirm that they really want to delete all + * site data for a number of hosts. + **/ +let gSiteDataRemoveSelected = { + init() { + document.addEventListener("dialogaccept", function () { + window.arguments[0].allowed = true; + }); + document.addEventListener("dialogcancel", function () { + window.arguments[0].allowed = false; + }); + + let list = document.getElementById("removalList"); + + let hosts = window.arguments[0].hosts; + + if (!hosts) { + throw new Error("Must specify hosts option in arguments."); + } + let dialog = document.getElementById("SiteDataRemoveSelectedDialog"); + if (hosts.length == 1) { + dialog.classList.add("single-entry"); + document.l10n.setAttributes( + document.getElementById("removing-description"), + "site-data-removing-single-desc", + { + baseDomain: hosts[0], + } + ); + return; + } + dialog.classList.add("multi-entry"); + hosts.sort(); + let fragment = document.createDocumentFragment(); + for (let host of hosts) { + let listItem = document.createXULElement("richlistitem"); + let label = document.createXULElement("label"); + if (host) { + label.setAttribute("value", host); + } else { + document.l10n.setAttributes(label, "site-data-local-file-host"); + } + listItem.appendChild(label); + fragment.appendChild(listItem); + } + list.appendChild(fragment); + }, +}; diff --git a/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml new file mode 100644 index 0000000000..56b50f3d53 --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/siteDataSettings.css" type="text/css"?> + +<window id="SiteDataRemoveSelectedDialog" + width="500" + data-l10n-id="site-data-removing-dialog" + data-l10n-attrs="title" + onload="gSiteDataRemoveSelected.init();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> +<dialog data-l10n-id="site-data-removing-dialog" + data-l10n-attrs="buttonlabelaccept"> + + <linkset> + <html:link rel="localization" href="browser/preferences/siteDataSettings.ftl"/> + </linkset> + + <hbox> + <vbox> + <image class="question-icon"/> + </vbox> + <vbox flex="1"> + <!-- Only show this label on OS X because of no dialog title --> + <label id="removing-label" + data-l10n-id="site-data-removing-header" +#ifndef XP_MACOSX + hidden="true" +#endif + /> + <separator class="thin"/> + <description id="removing-description" data-l10n-id="site-data-removing-desc"/> + </vbox> + </hbox> + <separator class="multi-site"/> + + <label data-l10n-id="site-data-removing-table" class="multi-site"/> + <separator class="thin multi-site"/> + <richlistbox id="removalList" class="theme-listbox multi-site" flex="1"/> + <!-- Load the script after the elements for layout issues (bug 1501755). --> + <script src="chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.js"/> +</dialog> +</window> diff --git a/browser/components/preferences/dialogs/siteDataSettings.js b/browser/components/preferences/dialogs/siteDataSettings.js new file mode 100644 index 0000000000..7c0c490e5d --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataSettings.js @@ -0,0 +1,335 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +ChromeUtils.defineModuleGetter( + this, + "SiteDataManager", + "resource:///modules/SiteDataManager.jsm" +); +ChromeUtils.defineESModuleGetters(this, { + DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", +}); + +let gSiteDataSettings = { + // Array of metadata of sites. Each array element is object holding: + // - uri: uri of site; instance of nsIURI + // - baseDomain: base domain of the site + // - cookies: array of cookies of that site + // - usage: disk usage which site uses + // - userAction: "remove" or "update-permission"; the action user wants to take. + _sites: null, + + _list: null, + _searchBox: null, + + _createSiteListItem(site) { + let item = document.createXULElement("richlistitem"); + item.setAttribute("host", site.baseDomain); + let container = document.createXULElement("hbox"); + + // Creates a new column item with the specified relative width. + function addColumnItem(l10n, flexWidth, tooltipText) { + let box = document.createXULElement("hbox"); + box.className = "item-box"; + box.setAttribute("style", `flex: ${flexWidth} ${flexWidth};`); + let label = document.createXULElement("label"); + label.setAttribute("crop", "end"); + if (l10n) { + if (l10n.hasOwnProperty("raw")) { + box.setAttribute("tooltiptext", l10n.raw); + label.setAttribute("value", l10n.raw); + } else { + document.l10n.setAttributes(label, l10n.id, l10n.args); + } + } + if (tooltipText) { + box.setAttribute("tooltiptext", tooltipText); + } + box.appendChild(label); + container.appendChild(box); + } + + // Add "Host" column. + let hostData = site.baseDomain + ? { raw: site.baseDomain } + : { id: "site-data-local-file-host" }; + addColumnItem(hostData, "4"); + + // Add "Cookies" column. + addColumnItem({ raw: site.cookies.length }, "1"); + + // Add "Storage" column + if (site.usage > 0 || site.persisted) { + let [value, unit] = DownloadUtils.convertByteUnits(site.usage); + let strName = site.persisted + ? "site-storage-persistent" + : "site-storage-usage"; + addColumnItem( + { + id: strName, + args: { value, unit }, + }, + "2" + ); + } else { + // Pass null to avoid showing "0KB" when there is no site data stored. + addColumnItem(null, "2"); + } + + // Add "Last Used" column. + let formattedLastAccessed = + site.lastAccessed > 0 + ? this._relativeTimeFormat.formatBestUnit(site.lastAccessed) + : null; + let formattedFullDate = + site.lastAccessed > 0 + ? this._absoluteTimeFormat.format(site.lastAccessed) + : null; + addColumnItem( + site.lastAccessed > 0 ? { raw: formattedLastAccessed } : null, + "2", + formattedFullDate + ); + + item.appendChild(container); + return item; + }, + + init() { + function setEventListener(id, eventType, callback) { + document + .getElementById(id) + .addEventListener(eventType, callback.bind(gSiteDataSettings)); + } + + this._absoluteTimeFormat = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "short", + }); + + this._relativeTimeFormat = new Services.intl.RelativeTimeFormat( + undefined, + {} + ); + + this._list = document.getElementById("sitesList"); + this._searchBox = document.getElementById("searchBox"); + SiteDataManager.getSites().then(sites => { + this._sites = sites; + let sortCol = document.querySelector( + "treecol[data-isCurrentSortCol=true]" + ); + this._sortSites(this._sites, sortCol); + this._buildSitesList(this._sites); + Services.obs.notifyObservers(null, "sitedata-settings-init"); + }); + + setEventListener("sitesList", "select", this.onSelect); + setEventListener("hostCol", "click", this.onClickTreeCol); + setEventListener("usageCol", "click", this.onClickTreeCol); + setEventListener("lastAccessedCol", "click", this.onClickTreeCol); + setEventListener("cookiesCol", "click", this.onClickTreeCol); + setEventListener("searchBox", "command", this.onCommandSearch); + setEventListener("removeAll", "command", this.onClickRemoveAll); + setEventListener("removeSelected", "command", this.removeSelected); + + document.addEventListener("dialogaccept", e => this.saveChanges(e)); + }, + + _updateButtonsState() { + let items = this._list.getElementsByTagName("richlistitem"); + let removeSelectedBtn = document.getElementById("removeSelected"); + let removeAllBtn = document.getElementById("removeAll"); + removeSelectedBtn.disabled = !this._list.selectedItems.length; + removeAllBtn.disabled = !items.length; + + let l10nId = this._searchBox.value + ? "site-data-remove-shown" + : "site-data-remove-all"; + document.l10n.setAttributes(removeAllBtn, l10nId); + }, + + /** + * @param sites {Array} + * @param col {XULElement} the <treecol> being sorted on + */ + _sortSites(sites, col) { + let isCurrentSortCol = col.getAttribute("data-isCurrentSortCol"); + let sortDirection = + col.getAttribute("data-last-sortDirection") || "ascending"; + if (isCurrentSortCol) { + // Sort on the current column, flip the sorting direction + sortDirection = + sortDirection === "ascending" ? "descending" : "ascending"; + } + + let sortFunc = null; + switch (col.id) { + case "hostCol": + sortFunc = (a, b) => { + let aHost = a.baseDomain.toLowerCase(); + let bHost = b.baseDomain.toLowerCase(); + return aHost.localeCompare(bHost); + }; + break; + + case "cookiesCol": + sortFunc = (a, b) => a.cookies.length - b.cookies.length; + break; + + case "usageCol": + sortFunc = (a, b) => a.usage - b.usage; + break; + + case "lastAccessedCol": + sortFunc = (a, b) => a.lastAccessed - b.lastAccessed; + break; + } + if (sortDirection === "descending") { + sites.sort((a, b) => sortFunc(b, a)); + } else { + sites.sort(sortFunc); + } + + let cols = this._list.previousElementSibling.querySelectorAll("treecol"); + cols.forEach(c => { + c.removeAttribute("sortDirection"); + c.removeAttribute("data-isCurrentSortCol"); + }); + col.setAttribute("data-isCurrentSortCol", true); + col.setAttribute("sortDirection", sortDirection); + col.setAttribute("data-last-sortDirection", sortDirection); + }, + + /** + * @param sites {Array} array of metadata of sites + */ + _buildSitesList(sites) { + // Clear old entries. + let oldItems = this._list.querySelectorAll("richlistitem"); + for (let item of oldItems) { + item.remove(); + } + + let keyword = this._searchBox.value.toLowerCase().trim(); + let fragment = document.createDocumentFragment(); + for (let site of sites) { + if (keyword && !site.baseDomain.includes(keyword)) { + continue; + } + + if (site.userAction === "remove") { + continue; + } + + let item = this._createSiteListItem(site); + fragment.appendChild(item); + } + this._list.appendChild(fragment); + this._updateButtonsState(); + }, + + _removeSiteItems(items) { + for (let i = items.length - 1; i >= 0; --i) { + let item = items[i]; + let baseDomain = item.getAttribute("host"); + let siteForBaseDomain = this._sites.find( + site => site.baseDomain == baseDomain + ); + if (siteForBaseDomain) { + siteForBaseDomain.userAction = "remove"; + } + item.remove(); + } + this._updateButtonsState(); + }, + + async saveChanges(event) { + let removals = this._sites + .filter(site => site.userAction == "remove") + .map(site => site.baseDomain); + + if (removals.length) { + let removeAll = removals.length == this._sites.length; + let promptArg = removeAll ? undefined : removals; + if (!SiteDataManager.promptSiteDataRemoval(window, promptArg)) { + // If the user cancelled the confirm dialog keep the site data window open, + // they can still press cancel again to exit. + event.preventDefault(); + return; + } + try { + if (removeAll) { + await SiteDataManager.removeAll(); + } else { + await SiteDataManager.remove(removals); + } + } catch (e) { + console.error(e); + } + } + }, + + removeSelected() { + let lastIndex = this._list.selectedItems.length - 1; + let lastSelectedItem = this._list.selectedItems[lastIndex]; + let lastSelectedItemPosition = this._list.getIndexOfItem(lastSelectedItem); + let nextSelectedItem = this._list.getItemAtIndex( + lastSelectedItemPosition + 1 + ); + + this._removeSiteItems(this._list.selectedItems); + this._list.clearSelection(); + + if (nextSelectedItem) { + this._list.selectedItem = nextSelectedItem; + } else { + this._list.selectedIndex = this._list.itemCount - 1; + } + }, + + onClickTreeCol(e) { + this._sortSites(this._sites, e.target); + this._buildSitesList(this._sites); + this._list.clearSelection(); + }, + + onCommandSearch() { + this._buildSitesList(this._sites); + this._list.clearSelection(); + }, + + onClickRemoveAll() { + let siteItems = this._list.getElementsByTagName("richlistitem"); + if (siteItems.length) { + this._removeSiteItems(siteItems); + } + }, + + onKeyPress(e) { + if ( + e.keyCode == KeyEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + e.keyCode == KeyEvent.DOM_VK_BACK_SPACE) + ) { + if (!e.target.closest("#sitesList")) { + // The user is typing or has not selected an item from the list to remove + return; + } + // The users intention is to delete site data + this.removeSelected(); + } + }, + + onSelect() { + this._updateButtonsState(); + }, +}; diff --git a/browser/components/preferences/dialogs/siteDataSettings.xhtml b/browser/components/preferences/dialogs/siteDataSettings.xhtml new file mode 100644 index 0000000000..251b70d3fe --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataSettings.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/siteDataSettings.css" type="text/css"?> + +<window + id="SiteDataSettingsDialog" + data-l10n-id="site-data-settings-window" + data-l10n-attrs="title" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + style="min-width: 45em" + onload="gSiteDataSettings.init();" + onkeypress="gSiteDataSettings.onKeyPress(event);" + persist="width height" +> + <dialog + buttons="accept,cancel" + data-l10n-id="site-data-settings-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="browser/preferences/siteDataSettings.ftl" + /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/siteDataSettings.js" /> + + <vbox flex="1" class="contentPane"> + <description + id="settingsDescription" + data-l10n-id="site-data-settings-description" + /> + <separator class="thin" /> + + <hbox id="searchBoxContainer"> + <search-textbox + id="searchBox" + flex="1" + data-l10n-id="site-data-search-textbox" + data-l10n-attrs="placeholder" + /> + </hbox> + <separator class="thin" /> + + <listheader> + <treecol + style="flex: 4 4 auto; width: 50px" + data-l10n-id="site-data-column-host" + id="hostCol" + /> + <treecol + style="flex: 1 auto; width: 50px" + data-l10n-id="site-data-column-cookies" + id="cookiesCol" + /> + <!-- Sorted by usage so the user can quickly see which sites use the most data. --> + <treecol + style="flex: 2 2 auto; width: 50px" + data-l10n-id="site-data-column-storage" + id="usageCol" + data-isCurrentSortCol="true" + /> + <treecol + style="flex: 2 2 auto; width: 50px" + data-l10n-id="site-data-column-last-used" + id="lastAccessedCol" + /> + </listheader> + <richlistbox seltype="multiple" id="sitesList" orient="vertical" /> + </vbox> + + <hbox align="start"> + <button id="removeSelected" data-l10n-id="site-data-remove-selected" /> + <button id="removeAll" /> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/sitePermissions.css b/browser/components/preferences/dialogs/sitePermissions.css new file mode 100644 index 0000000000..e41c7203fe --- /dev/null +++ b/browser/components/preferences/dialogs/sitePermissions.css @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.website-name, +hbox.website-status { + overflow: hidden; /* Allows equal sizing combined with width="0" */ + padding-inline-start: 7px; + align-items: center; +} + +#permissionsBox { + flex: 1 auto; + height: 18em; + min-height: 70px; /* 2 * 35px, which is the min row height specified below */ +} + +#siteCol, +#statusCol, +#permissionsBox > richlistitem { + min-height: 35px; +} + +#permissionsBox > richlistitem > hbox { + flex: 1; +} + +#siteCol { + flex: 3 3 auto; +} + +.website-name { + flex: 3 3; +} + +#statusCol { + flex: 1 auto; +} + +.website-status { + flex: 1; +} + +/* TODO(bug 1802993): Seems this could be on .website-name instead of label? */ +#siteCol, +#statusCol, +.website-name > label, +.website-status { + width: 75px; +} + +menulist.website-status { + margin: 1px; + margin-inline-end: 5px; +} + +#browserNotificationsPermissionExtensionContent, +#permissionsDisableDescription { + margin-inline-start: 32px; +} + +#permissionsDisableDescription { + color: var(--text-color-deemphasized); + line-height: 110%; +} + +#permissionsDisableCheckbox { + margin-inline-start: 4px; + padding-top: 10px; +} diff --git a/browser/components/preferences/dialogs/sitePermissions.js b/browser/components/preferences/dialogs/sitePermissions.js new file mode 100644 index 0000000000..61af43463b --- /dev/null +++ b/browser/components/preferences/dialogs/sitePermissions.js @@ -0,0 +1,679 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../extensionControlled.js */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { SitePermissions } = ChromeUtils.importESModule( + "resource:///modules/SitePermissions.sys.mjs" +); + +const sitePermissionsL10n = { + "desktop-notification": { + window: "permissions-site-notification-window2", + description: "permissions-site-notification-desc", + disableLabel: "permissions-site-notification-disable-label", + disableDescription: "permissions-site-notification-disable-desc", + }, + geo: { + window: "permissions-site-location-window2", + description: "permissions-site-location-desc", + disableLabel: "permissions-site-location-disable-label", + disableDescription: "permissions-site-location-disable-desc", + }, + xr: { + window: "permissions-site-xr-window2", + description: "permissions-site-xr-desc", + disableLabel: "permissions-site-xr-disable-label", + disableDescription: "permissions-site-xr-disable-desc", + }, + camera: { + window: "permissions-site-camera-window2", + description: "permissions-site-camera-desc", + disableLabel: "permissions-site-camera-disable-label", + disableDescription: "permissions-site-camera-disable-desc", + }, + microphone: { + window: "permissions-site-microphone-window2", + description: "permissions-site-microphone-desc", + disableLabel: "permissions-site-microphone-disable-label", + disableDescription: "permissions-site-microphone-disable-desc", + }, + speaker: { + window: "permissions-site-speaker-window", + description: "permissions-site-speaker-desc", + }, + "autoplay-media": { + window: "permissions-site-autoplay-window2", + description: "permissions-site-autoplay-desc", + }, +}; + +const sitePermissionsConfig = { + "autoplay-media": { + _getCapabilityString(capability) { + switch (capability) { + case SitePermissions.ALLOW: + return "permissions-capabilities-autoplay-allow"; + case SitePermissions.BLOCK: + return "permissions-capabilities-autoplay-block"; + case SitePermissions.AUTOPLAY_BLOCKED_ALL: + return "permissions-capabilities-autoplay-blockall"; + } + throw new Error(`Unknown capability: ${capability}`); + }, + }, +}; + +// A set of permissions for a single origin. One PermissionGroup instance +// corresponds to one row in the gSitePermissionsManager._list richlistbox. +// Permissions may be single or double keyed, but the primary key of all +// permissions matches the permission type of the dialog. +class PermissionGroup { + #changedCapability; + + constructor(perm) { + this.principal = perm.principal; + this.origin = perm.principal.origin; + this.perms = [perm]; + } + addPermission(perm) { + this.perms.push(perm); + } + removePermission(perm) { + this.perms = this.perms.filter(p => p.type != perm.type); + } + set capability(cap) { + this.#changedCapability = cap; + } + get capability() { + if (this.#changedCapability) { + return this.#changedCapability; + } + return this.savedCapability; + } + revert() { + this.#changedCapability = null; + } + get savedCapability() { + // This logic to present a single capability for permissions of different + // keys and capabilities caters for speaker-selection, where a block + // permission may be set for all devices with no second key, which would + // override any device-specific double-keyed allow permissions. + let cap; + for (let perm of this.perms) { + let [type] = perm.type.split(SitePermissions.PERM_KEY_DELIMITER); + if (type == perm.type) { + // No second key. This overrides double-keyed perms. + return perm.capability; + } + // Double-keyed perms are not expected to have different capabilities. + cap = perm.capability; + } + return cap; + } +} + +const PERMISSION_STATES = [ + SitePermissions.ALLOW, + SitePermissions.BLOCK, + SitePermissions.PROMPT, + SitePermissions.AUTOPLAY_BLOCKED_ALL, +]; + +const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled"; +const NOTIFICATIONS_PERMISSION_PREF = + "permissions.default.desktop-notification"; + +const AUTOPLAY_PREF = "media.autoplay.default"; + +var gSitePermissionsManager = { + _type: "", + _isObserving: false, + _permissionGroups: new Map(), + _permissionsToChange: new Map(), + _permissionsToDelete: new Map(), + _list: null, + _removeButton: null, + _removeAllButton: null, + _searchBox: null, + _checkbox: null, + _currentDefaultPermissionsState: null, + _defaultPermissionStatePrefName: null, + + onLoad() { + let params = window.arguments[0]; + document.mozSubdialogReady = this.init(params); + }, + + async init(params) { + if (!this._isObserving) { + Services.obs.addObserver(this, "perm-changed"); + this._isObserving = true; + } + + document.addEventListener("dialogaccept", () => this.onApplyChanges()); + + this._type = params.permissionType; + this._list = document.getElementById("permissionsBox"); + this._removeButton = document.getElementById("removePermission"); + this._removeAllButton = document.getElementById("removeAllPermissions"); + this._searchBox = document.getElementById("searchBox"); + this._checkbox = document.getElementById("permissionsDisableCheckbox"); + this._disableExtensionButton = document.getElementById( + "disableNotificationsPermissionExtension" + ); + this._permissionsDisableDescription = document.getElementById( + "permissionsDisableDescription" + ); + this._setAutoplayPref = document.getElementById("setAutoplayPref"); + + let permissionsText = document.getElementById("permissionsText"); + + document.l10n.pauseObserving(); + let l10n = sitePermissionsL10n[this._type]; + document.l10n.setAttributes(permissionsText, l10n.description); + if (l10n.disableLabel) { + document.l10n.setAttributes(this._checkbox, l10n.disableLabel); + } + if (l10n.disableDescription) { + document.l10n.setAttributes( + this._permissionsDisableDescription, + l10n.disableDescription + ); + } + document.l10n.setAttributes(document.documentElement, l10n.window); + + await document.l10n.translateElements([ + permissionsText, + this._checkbox, + this._permissionsDisableDescription, + document.documentElement, + ]); + document.l10n.resumeObserving(); + + // Initialize the checkbox state and handle showing notification permission UI + // when it is disabled by an extension. + this._defaultPermissionStatePrefName = "permissions.default." + this._type; + this._watchPermissionPrefChange(); + + this._loadPermissions(); + this.buildPermissionsList(); + + if (params.permissionType == "autoplay-media") { + await this.buildAutoplayMenulist(); + this._setAutoplayPref.hidden = false; + } + + this._searchBox.focus(); + }, + + uninit() { + if (this._isObserving) { + Services.obs.removeObserver(this, "perm-changed"); + this._isObserving = false; + } + if (this._setAutoplayPref) { + this._setAutoplayPref.hidden = true; + } + }, + + observe(subject, topic, data) { + if (topic !== "perm-changed") { + return; + } + + let permission = subject.QueryInterface(Ci.nsIPermission); + let [type] = permission.type.split(SitePermissions.PERM_KEY_DELIMITER); + + // Ignore unrelated permission types and permissions with unknown states. + if ( + type !== this._type || + !PERMISSION_STATES.includes(permission.capability) + ) { + return; + } + + if (data == "added") { + this._addPermissionToList(permission); + } else { + let group = this._permissionGroups.get(permission.principal.origin); + if (!group) { + // already moved to _permissionsToDelete + // or private browsing session permission + return; + } + if (data == "changed") { + group.removePermission(permission); + group.addPermission(permission); + } else if (data == "deleted") { + group.removePermission(permission); + if (!group.perms.length) { + this._removePermissionFromList(permission.principal.origin); + return; + } + } + } + this.buildPermissionsList(); + }, + + _handleCheckboxUIUpdates() { + let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName); + if (pref != Services.prefs.PREF_INVALID) { + this._currentDefaultPermissionsState = Services.prefs.getIntPref( + this._defaultPermissionStatePrefName + ); + } + + if (this._currentDefaultPermissionsState === null) { + this._checkbox.hidden = true; + this._permissionsDisableDescription.hidden = true; + } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) { + this._checkbox.checked = true; + } else { + this._checkbox.checked = false; + } + + if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) { + this._checkbox.disabled = true; + } + }, + + /** + * Listen for changes to the permissions.default.* pref and make + * necessary changes to the UI. + */ + _watchPermissionPrefChange() { + this._handleCheckboxUIUpdates(); + + if (this._type == "desktop-notification") { + this._handleWebNotificationsDisable(); + + this._disableExtensionButton.addEventListener( + "command", + makeDisableControllingExtension( + PREF_SETTING_TYPE, + NOTIFICATIONS_PERMISSION_OVERRIDE_KEY + ) + ); + } + + let observer = () => { + this._handleCheckboxUIUpdates(); + if (this._type == "desktop-notification") { + this._handleWebNotificationsDisable(); + } + }; + Services.prefs.addObserver(this._defaultPermissionStatePrefName, observer); + window.addEventListener("unload", () => { + Services.prefs.removeObserver( + this._defaultPermissionStatePrefName, + observer + ); + }); + }, + + /** + * Handles the UI update for web notifications disable by extensions. + */ + async _handleWebNotificationsDisable() { + let prefLocked = Services.prefs.prefIsLocked(NOTIFICATIONS_PERMISSION_PREF); + if (prefLocked) { + // An extension can't control these settings if they're locked. + hideControllingExtension(NOTIFICATIONS_PERMISSION_OVERRIDE_KEY); + } else { + let isControlled = await handleControllingExtension( + PREF_SETTING_TYPE, + NOTIFICATIONS_PERMISSION_OVERRIDE_KEY + ); + this._checkbox.disabled = isControlled; + } + }, + + _getCapabilityL10nId(element, type, capability) { + if ( + type in sitePermissionsConfig && + sitePermissionsConfig[type]._getCapabilityString + ) { + return sitePermissionsConfig[type]._getCapabilityString(capability); + } + switch (element.tagName) { + case "menuitem": + switch (capability) { + case Services.perms.ALLOW_ACTION: + return "permissions-capabilities-allow"; + case Services.perms.DENY_ACTION: + return "permissions-capabilities-block"; + case Services.perms.PROMPT_ACTION: + return "permissions-capabilities-prompt"; + default: + throw new Error(`Unknown capability: ${capability}`); + } + case "label": + switch (capability) { + case Services.perms.ALLOW_ACTION: + return "permissions-capabilities-listitem-allow"; + case Services.perms.DENY_ACTION: + return "permissions-capabilities-listitem-block"; + default: + throw new Error(`Unexpected capability: ${capability}`); + } + default: + throw new Error(`Unexpected tag: ${element.tagName}`); + } + }, + + _addPermissionToList(perm) { + let [type] = perm.type.split(SitePermissions.PERM_KEY_DELIMITER); + // Ignore unrelated permission types and permissions with unknown states. + if ( + type !== this._type || + !PERMISSION_STATES.includes(perm.capability) || + // Skip private browsing session permissions + (perm.principal.privateBrowsingId !== + Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID && + perm.expireType === Services.perms.EXPIRE_SESSION) + ) { + return; + } + let group = this._permissionGroups.get(perm.principal.origin); + if (group) { + group.addPermission(perm); + } else { + group = new PermissionGroup(perm); + this._permissionGroups.set(group.origin, group); + } + }, + + _removePermissionFromList(origin) { + this._permissionGroups.delete(origin); + this._permissionsToChange.delete(origin); + let permissionlistitem = document.getElementsByAttribute( + "origin", + origin + )[0]; + if (permissionlistitem) { + permissionlistitem.remove(); + } + }, + + _loadPermissions() { + // load permissions into a table. + for (let nextPermission of Services.perms.all) { + this._addPermissionToList(nextPermission); + } + }, + + _createPermissionListItem(permissionGroup) { + let richlistitem = document.createXULElement("richlistitem"); + richlistitem.setAttribute("origin", permissionGroup.origin); + let row = document.createXULElement("hbox"); + + let hbox = document.createXULElement("hbox"); + let website = document.createXULElement("label"); + website.setAttribute("value", permissionGroup.origin); + hbox.setAttribute("class", "website-name"); + hbox.appendChild(website); + + let states = SitePermissions.getAvailableStates(this._type).filter( + state => state != SitePermissions.UNKNOWN + ); + // Handle the cases of a double-keyed ALLOW permission or a PROMPT + // permission after the default has been changed back to UNKNOWN. + if (!states.includes(permissionGroup.savedCapability)) { + states.unshift(permissionGroup.savedCapability); + } + let siteStatus; + if (states.length == 1) { + // Only a single state is available. Show a label. + siteStatus = document.createXULElement("hbox"); + let label = document.createXULElement("label"); + siteStatus.appendChild(label); + document.l10n.setAttributes( + label, + this._getCapabilityL10nId(label, this._type, permissionGroup.capability) + ); + } else { + // Multiple states are available. Show a menulist. + siteStatus = document.createXULElement("menulist"); + for (let state of states) { + let m = siteStatus.appendItem(undefined, state); + document.l10n.setAttributes( + m, + this._getCapabilityL10nId(m, this._type, state) + ); + } + siteStatus.addEventListener("select", () => { + this.onPermissionChange(permissionGroup, Number(siteStatus.value)); + }); + } + siteStatus.setAttribute("class", "website-status"); + siteStatus.value = permissionGroup.capability; + + row.appendChild(hbox); + row.appendChild(siteStatus); + richlistitem.appendChild(row); + return richlistitem; + }, + + onPermissionKeyPress(event) { + if (!this._list.selectedItem) { + return; + } + + if ( + event.keyCode == KeyEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + event.keyCode == KeyEvent.DOM_VK_BACK_SPACE) + ) { + this.onPermissionDelete(); + event.preventDefault(); + } + }, + + _setRemoveButtonState() { + if (!this._list) { + return; + } + + let hasSelection = this._list.selectedIndex >= 0; + let hasRows = this._list.itemCount > 0; + this._removeButton.disabled = !hasSelection; + this._removeAllButton.disabled = !hasRows; + }, + + onPermissionDelete() { + let richlistitem = this._list.selectedItem; + let origin = richlistitem.getAttribute("origin"); + let permissionGroup = this._permissionGroups.get(origin); + + this._removePermissionFromList(origin); + this._permissionsToDelete.set(permissionGroup.origin, permissionGroup); + + this._setRemoveButtonState(); + }, + + onAllPermissionsDelete() { + for (let permissionGroup of this._permissionGroups.values()) { + this._removePermissionFromList(permissionGroup.origin); + this._permissionsToDelete.set(permissionGroup.origin, permissionGroup); + } + + this._setRemoveButtonState(); + }, + + onPermissionSelect() { + this._setRemoveButtonState(); + }, + + onPermissionChange(perm, capability) { + let group = this._permissionGroups.get(perm.origin); + if (group.capability == capability) { + return; + } + if (capability == group.savedCapability) { + group.revert(); + this._permissionsToChange.delete(group.origin); + } else { + group.capability = capability; + this._permissionsToChange.set(group.origin, group); + } + + // enable "remove all" button as needed + this._setRemoveButtonState(); + }, + + onApplyChanges() { + // Stop observing permission changes since we are about + // to write out the pending adds/deletes and don't need + // to update the UI + this.uninit(); + + // Delete even _permissionsToChange to clear out double-keyed permissions + for (let group of [ + ...this._permissionsToDelete.values(), + ...this._permissionsToChange.values(), + ]) { + for (let perm of group.perms) { + SitePermissions.removeFromPrincipal(perm.principal, perm.type); + } + } + + for (let group of this._permissionsToChange.values()) { + SitePermissions.setForPrincipal( + group.principal, + this._type, + group.capability + ); + } + + if (this._checkbox.checked) { + Services.prefs.setIntPref( + this._defaultPermissionStatePrefName, + SitePermissions.BLOCK + ); + } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) { + Services.prefs.setIntPref( + this._defaultPermissionStatePrefName, + SitePermissions.UNKNOWN + ); + } + }, + + buildPermissionsList(sortCol) { + // Clear old entries. + let oldItems = this._list.querySelectorAll("richlistitem"); + for (let item of oldItems) { + item.remove(); + } + let frag = document.createDocumentFragment(); + + let permissionGroups = Array.from(this._permissionGroups.values()); + + let keyword = this._searchBox.value.toLowerCase().trim(); + for (let permissionGroup of permissionGroups) { + if (keyword && !permissionGroup.origin.includes(keyword)) { + continue; + } + + let richlistitem = this._createPermissionListItem(permissionGroup); + frag.appendChild(richlistitem); + } + + // Sort permissions. + this._sortPermissions(this._list, frag, sortCol); + + this._list.appendChild(frag); + + this._setRemoveButtonState(); + }, + + async buildAutoplayMenulist() { + let menulist = document.createXULElement("menulist"); + let states = SitePermissions.getAvailableStates("autoplay-media"); + document.l10n.pauseObserving(); + for (let state of states) { + let m = menulist.appendItem(undefined, state); + document.l10n.setAttributes( + m, + this._getCapabilityL10nId(m, "autoplay-media", state) + ); + } + + menulist.value = SitePermissions.getDefault("autoplay-media"); + + menulist.addEventListener("select", () => { + SitePermissions.setDefault("autoplay-media", Number(menulist.value)); + }); + + menulist.menupopup.setAttribute("incontentshell", "false"); + + menulist.disabled = Services.prefs.prefIsLocked(AUTOPLAY_PREF); + + document.getElementById("setAutoplayPref").appendChild(menulist); + await document.l10n.translateFragment(menulist); + document.l10n.resumeObserving(); + }, + + _sortPermissions(list, frag, column) { + let sortDirection; + + if (!column) { + column = document.querySelector("treecol[data-isCurrentSortCol=true]"); + sortDirection = + column.getAttribute("data-last-sortDirection") || "ascending"; + } else { + sortDirection = column.getAttribute("data-last-sortDirection"); + sortDirection = + sortDirection === "ascending" ? "descending" : "ascending"; + } + + let sortFunc = null; + switch (column.id) { + case "siteCol": + sortFunc = (a, b) => { + return comp.compare( + a.getAttribute("origin"), + b.getAttribute("origin") + ); + }; + break; + + case "statusCol": + sortFunc = (a, b) => { + return ( + parseInt(a.querySelector(".website-status").value) > + parseInt(b.querySelector(".website-status").value) + ); + }; + break; + } + + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + let items = Array.from(frag.querySelectorAll("richlistitem")); + + if (sortDirection === "descending") { + items.sort((a, b) => sortFunc(b, a)); + } else { + items.sort(sortFunc); + } + + // Re-append items in the correct order: + items.forEach(item => frag.appendChild(item)); + + let cols = list.previousElementSibling.querySelectorAll("treecol"); + cols.forEach(c => { + c.removeAttribute("data-isCurrentSortCol"); + c.removeAttribute("sortDirection"); + }); + column.setAttribute("data-isCurrentSortCol", "true"); + column.setAttribute("sortDirection", sortDirection); + column.setAttribute("data-last-sortDirection", sortDirection); + }, +}; diff --git a/browser/components/preferences/dialogs/sitePermissions.xhtml b/browser/components/preferences/dialogs/sitePermissions.xhtml new file mode 100644 index 0000000000..5cc307aced --- /dev/null +++ b/browser/components/preferences/dialogs/sitePermissions.xhtml @@ -0,0 +1,115 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window + id="SitePermissionsDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="permissions-window2" + data-l10n-attrs="title, style" + onload="gSitePermissionsManager.onLoad();" + onunload="gSitePermissionsManager.uninit();" + persist="width height" +> + <dialog + buttons="accept,cancel" + data-l10n-id="permission-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link + rel="localization" + href="browser/preferences/preferences.ftl" + /> + <html:link + rel="localization" + href="browser/preferences/permissions.ftl" + /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/sitePermissions.js" /> + <script src="chrome://browser/content/preferences/extensionControlled.js" /> + + <keyset> + <key + data-l10n-id="permissions-close-key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane"> + <hbox align="center" id="setAutoplayPref" hidden="true"> + <label data-l10n-id="permissions-autoplay-menu" /> + </hbox> + <description id="permissionsText" control="url" /> + <separator class="thin" /> + <hbox align="start"> + <search-textbox + id="searchBox" + flex="1" + data-l10n-id="permissions-searchbox" + data-l10n-attrs="placeholder" + oncommand="gSitePermissionsManager.buildPermissionsList();" + /> + </hbox> + <separator class="thin" /> + <listheader> + <treecol + id="siteCol" + data-l10n-id="permissions-site-name" + onclick="gSitePermissionsManager.buildPermissionsList(event.target)" + /> + <treecol + id="statusCol" + data-l10n-id="permissions-status" + data-isCurrentSortCol="true" + onclick="gSitePermissionsManager.buildPermissionsList(event.target);" + /> + </listheader> + <richlistbox + id="permissionsBox" + selected="false" + onkeypress="gSitePermissionsManager.onPermissionKeyPress(event);" + onselect="gSitePermissionsManager.onPermissionSelect();" + /> + </vbox> + + <hbox class="actionButtons"> + <button + id="removePermission" + disabled="true" + data-l10n-id="permissions-remove" + oncommand="gSitePermissionsManager.onPermissionDelete();" + /> + <button + id="removeAllPermissions" + data-l10n-id="permissions-remove-all" + oncommand="gSitePermissionsManager.onAllPermissionsDelete();" + /> + </hbox> + + <checkbox id="permissionsDisableCheckbox" /> + <description id="permissionsDisableDescription" /> + <hbox + id="browserNotificationsPermissionExtensionContent" + class="extension-controlled" + align="center" + hidden="true" + > + <description control="disableNotificationsPermissionExtension" flex="1" /> + <button + id="disableNotificationsPermissionExtension" + class="extension-controlled-button accessory-button" + data-l10n-id="disable-extension" + /> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/syncChooseWhatToSync.js b/browser/components/preferences/dialogs/syncChooseWhatToSync.js new file mode 100644 index 0000000000..2cc965b4e1 --- /dev/null +++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "services.sync.engine.addons", type: "bool" }, + { id: "services.sync.engine.bookmarks", type: "bool" }, + { id: "services.sync.engine.history", type: "bool" }, + { id: "services.sync.engine.tabs", type: "bool" }, + { id: "services.sync.engine.prefs", type: "bool" }, + { id: "services.sync.engine.passwords", type: "bool" }, + { id: "services.sync.engine.addresses", type: "bool" }, + { id: "services.sync.engine.creditcards", type: "bool" }, +]); + +let gSyncChooseWhatToSync = { + init() { + this._adjustForPrefs(); + let options = window.arguments[0]; + if (options.disconnectFun) { + // We offer 'disconnect' + document.addEventListener("dialogextra2", function () { + options.disconnectFun().then(disconnected => { + if (disconnected) { + window.close(); + } + }); + }); + } else { + // no "disconnect" - hide the button. + document + .getElementById("syncChooseOptions") + .getButton("extra2").hidden = true; + } + }, + + // make whatever tweaks we need based on preferences. + _adjustForPrefs() { + // These 2 engines are unique in that there are prefs that make the + // entire engine unavailable (which is distinct from "disabled"). + let enginePrefs = [ + ["services.sync.engine.addresses", ".sync-engine-addresses"], + ["services.sync.engine.creditcards", ".sync-engine-creditcards"], + ]; + for (let [enabledPref, className] of enginePrefs) { + let availablePref = enabledPref + ".available"; + // If the engine is enabled we force it to be available, otherwise we see + // spooky things happen (like it magically re-appear later) + if (Services.prefs.getBoolPref(enabledPref, false)) { + Services.prefs.setBoolPref(availablePref, true); + } + if (!Services.prefs.getBoolPref(availablePref)) { + let elt = document.querySelector(className); + elt.hidden = true; + } + } + }, +}; diff --git a/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml new file mode 100644 index 0000000000..0bed6913d4 --- /dev/null +++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml @@ -0,0 +1,88 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gSyncChooseWhatToSync.init();" + data-l10n-id="sync-choose-what-to-sync-dialog3" + data-l10n-attrs="title, style" +> + <dialog + id="syncChooseOptions" + buttons="accept,cancel,extra2" + data-l10n-id="sync-choose-what-to-sync-dialog3" + data-l10n-attrs="buttonlabelaccept, buttonlabelextra2" + > + <linkset> + <html:link + rel="localization" + href="browser/preferences/preferences.ftl" + /> + <html:link rel="localization" href="toolkit/branding/accounts.ftl" /> + </linkset> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js" /> + <description + class="sync-choose-dialog-description" + data-l10n-id="sync-choose-dialog-subtitle" + /> + <html:div class="sync-engines-list"> + <html:div class="sync-engine-bookmarks"> + <checkbox + data-l10n-id="sync-engine-bookmarks" + preference="services.sync.engine.bookmarks" + /> + </html:div> + <html:div class="sync-engine-history"> + <checkbox + data-l10n-id="sync-engine-history" + preference="services.sync.engine.history" + /> + </html:div> + <html:div class="sync-engine-tabs"> + <checkbox + data-l10n-id="sync-engine-tabs" + preference="services.sync.engine.tabs" + /> + </html:div> + <html:div class="sync-engine-passwords"> + <checkbox + data-l10n-id="sync-engine-logins-passwords" + preference="services.sync.engine.passwords" + /> + </html:div> + <html:div class="sync-engine-addresses"> + <checkbox + data-l10n-id="sync-engine-addresses" + preference="services.sync.engine.addresses" + /> + </html:div> + <html:div class="sync-engine-creditcards"> + <checkbox + data-l10n-id="sync-engine-creditcards" + preference="services.sync.engine.creditcards" + /> + </html:div> + <html:div class="sync-engine-addons"> + <checkbox + data-l10n-id="sync-engine-addons" + preference="services.sync.engine.addons" + /> + </html:div> + <html:div class="sync-engine-prefs"> + <checkbox + data-l10n-id="sync-engine-settings" + preference="services.sync.engine.prefs" + /> + </html:div> + </html:div> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/translationExceptions.js b/browser/components/preferences/dialogs/translationExceptions.js new file mode 100644 index 0000000000..27579594c9 --- /dev/null +++ b/browser/components/preferences/dialogs/translationExceptions.js @@ -0,0 +1,256 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// TODO (Bug 1817084) Remove this file when we disable the extension + +"use strict"; + +const kPermissionType = "translate"; +const kLanguagesPref = "browser.translation.neverForLanguages"; + +function Tree(aId, aData) { + this._data = aData; + this._tree = document.getElementById(aId); + this._tree.view = this; +} + +Tree.prototype = { + get tree() { + return this._tree; + }, + get isEmpty() { + return !this._data.length; + }, + get hasSelection() { + return this.selection.count > 0; + }, + getSelectedItems() { + let result = []; + + let rc = this.selection.getRangeCount(); + for (let i = 0; i < rc; ++i) { + let min = {}, + max = {}; + this.selection.getRangeAt(i, min, max); + for (let j = min.value; j <= max.value; ++j) { + result.push(this._data[j]); + } + } + + return result; + }, + + // nsITreeView implementation + get rowCount() { + return this._data.length; + }, + getCellText(aRow, aColumn) { + return this._data[aRow]; + }, + isSeparator(aIndex) { + return false; + }, + isSorted() { + return false; + }, + isContainer(aIndex) { + return false; + }, + setTree(aTree) {}, + getImageSrc(aRow, aColumn) {}, + getCellValue(aRow, aColumn) {}, + cycleHeader(column) {}, + getRowProperties(row) { + return ""; + }, + getColumnProperties(column) { + return ""; + }, + getCellProperties(row, column) { + return ""; + }, + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), +}; + +function Lang(aCode, label) { + this.langCode = aCode; + this._label = label; +} + +Lang.prototype = { + toString() { + return this._label; + }, +}; + +var gTranslationExceptions = { + onLoad() { + if (this._siteTree) { + // Re-using an open dialog, clear the old observers. + this.uninit(); + } + + // Load site permissions into an array. + this._sites = []; + for (let perm of Services.perms.all) { + if ( + perm.type == kPermissionType && + perm.capability == Services.perms.DENY_ACTION + ) { + this._sites.push(perm.principal.origin); + } + } + Services.obs.addObserver(this, "perm-changed"); + this._sites.sort(); + + this._siteTree = new Tree("sitesTree", this._sites); + this.onSiteSelected(); + + this._langs = this.getLanguageExceptions(); + Services.prefs.addObserver(kLanguagesPref, this); + this._langTree = new Tree("languagesTree", this._langs); + this.onLanguageSelected(); + }, + + // Get the list of languages we don't translate as an array. + getLanguageExceptions() { + let langs = Services.prefs.getCharPref(kLanguagesPref); + if (!langs) { + return []; + } + + let langArr = langs.split(","); + let displayNames = Services.intl.getLanguageDisplayNames( + undefined, + langArr + ); + let result = langArr.map((lang, i) => new Lang(lang, displayNames[i])); + result.sort(); + + return result; + }, + + observe(aSubject, aTopic, aData) { + if (aTopic == "perm-changed") { + if (aData == "cleared") { + if (!this._sites.length) { + return; + } + let removed = this._sites.splice(0, this._sites.length); + this._siteTree.tree.rowCountChanged(0, -removed.length); + } else { + let perm = aSubject.QueryInterface(Ci.nsIPermission); + if (perm.type != kPermissionType) { + return; + } + + if (aData == "added") { + if (perm.capability != Services.perms.DENY_ACTION) { + return; + } + this._sites.push(perm.principal.origin); + this._sites.sort(); + let tree = this._siteTree.tree; + tree.rowCountChanged(0, 1); + tree.invalidate(); + } else if (aData == "deleted") { + let index = this._sites.indexOf(perm.principal.origin); + if (index == -1) { + return; + } + this._sites.splice(index, 1); + this._siteTree.tree.rowCountChanged(index, -1); + this.onSiteSelected(); + return; + } + } + this.onSiteSelected(); + } else if (aTopic == "nsPref:changed") { + this._langs = this.getLanguageExceptions(); + let change = this._langs.length - this._langTree.rowCount; + this._langTree._data = this._langs; + let tree = this._langTree.tree; + if (change) { + tree.rowCountChanged(0, change); + } + tree.invalidate(); + this.onLanguageSelected(); + } + }, + + _handleButtonDisabling(aTree, aIdPart) { + let empty = aTree.isEmpty; + document.getElementById("removeAll" + aIdPart + "s").disabled = empty; + document.getElementById("remove" + aIdPart).disabled = + empty || !aTree.hasSelection; + }, + + onLanguageSelected() { + this._handleButtonDisabling(this._langTree, "Language"); + }, + + onSiteSelected() { + this._handleButtonDisabling(this._siteTree, "Site"); + }, + + onLanguageDeleted() { + let langs = Services.prefs.getCharPref(kLanguagesPref); + if (!langs) { + return; + } + + let removed = this._langTree.getSelectedItems().map(l => l.langCode); + + langs = langs.split(",").filter(l => !removed.includes(l)); + Services.prefs.setCharPref(kLanguagesPref, langs.join(",")); + }, + + onAllLanguagesDeleted() { + Services.prefs.setCharPref(kLanguagesPref, ""); + }, + + onSiteDeleted() { + let removedSites = this._siteTree.getSelectedItems(); + for (let origin of removedSites) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + Services.perms.removeFromPrincipal(principal, kPermissionType); + } + }, + + onAllSitesDeleted() { + if (this._siteTree.isEmpty) { + return; + } + + let removedSites = this._sites.splice(0, this._sites.length); + this._siteTree.tree.rowCountChanged(0, -removedSites.length); + + for (let origin of removedSites) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + Services.perms.removeFromPrincipal(principal, kPermissionType); + } + + this.onSiteSelected(); + }, + + onSiteKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onSiteDeleted(); + } + }, + + onLanguageKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onLanguageDeleted(); + } + }, + + uninit() { + Services.obs.removeObserver(this, "perm-changed"); + Services.prefs.removeObserver(kLanguagesPref, this); + }, +}; diff --git a/browser/components/preferences/dialogs/translationExceptions.xhtml b/browser/components/preferences/dialogs/translationExceptions.xhtml new file mode 100644 index 0000000000..b824ce86a5 --- /dev/null +++ b/browser/components/preferences/dialogs/translationExceptions.xhtml @@ -0,0 +1,127 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!-- TODO (Bug 1817084) Remove this file when we disable the extension --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window + id="TranslationDialog" + data-l10n-id="translation-window2" + data-l10n-attrs="title, style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gTranslationExceptions.onLoad();" + onunload="gTranslationExceptions.uninit();" + persist="width height" +> + <dialog + buttons="accept" + data-l10n-id="translation-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link + rel="localization" + href="browser/preferences/translation.ftl" + /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/translationExceptions.js" /> + + <keyset> + <key + data-l10n-id="translation-close-key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane"> + <vbox flex="1"> + <label + id="languagesLabel" + data-l10n-id="translation-languages-disabled-desc" + control="permissionsTree" + /> + <separator class="thin" /> + <tree + id="languagesTree" + flex="1" + style="height: 12em" + hidecolumnpicker="true" + onkeypress="gTranslationExceptions.onLanguageKeyPress(event)" + onselect="gTranslationExceptions.onLanguageSelected();" + > + <treecols> + <treecol + id="languageCol" + data-l10n-id="translation-languages-column" + flex="1" + /> + </treecols> + <treechildren /> + </tree> + </vbox> + <hbox class="actionButtons" pack="end"> + <button + id="removeLanguage" + disabled="true" + data-l10n-id="translation-languages-button-remove" + oncommand="gTranslationExceptions.onLanguageDeleted();" + /> + <button + id="removeAllLanguages" + data-l10n-id="translation-languages-button-remove-all" + oncommand="gTranslationExceptions.onAllLanguagesDeleted();" + /> + <spacer flex="1" /> + </hbox> + <separator /> + <vbox flex="1"> + <label + id="languagesLabel" + data-l10n-id="translation-sites-disabled-desc" + control="permissionsTree" + /> + <separator class="thin" /> + <tree + id="sitesTree" + flex="1" + style="height: 12em" + hidecolumnpicker="true" + onkeypress="gTranslationExceptions.onSiteKeyPress(event)" + onselect="gTranslationExceptions.onSiteSelected();" + > + <treecols> + <treecol + id="siteCol" + data-l10n-id="translation-sites-column" + flex="1" + /> + </treecols> + <treechildren /> + </tree> + </vbox> + </vbox> + + <hbox class="actionButtons" pack="end"> + <button + id="removeSite" + disabled="true" + data-l10n-id="translation-sites-button-remove" + oncommand="gTranslationExceptions.onSiteDeleted();" + /> + <button + id="removeAllSites" + data-l10n-id="translation-sites-button-remove-all" + oncommand="gTranslationExceptions.onAllSitesDeleted();" + /> + <spacer flex="1" /> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/translations.js b/browser/components/preferences/dialogs/translations.js new file mode 100644 index 0000000000..30d2b22f17 --- /dev/null +++ b/browser/components/preferences/dialogs/translations.js @@ -0,0 +1,465 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * The permission type to give to Services.perms for Translations. + */ +const TRANSLATIONS_PERMISSION = "translations"; +/** + * The list of BCP-47 language tags that will trigger auto-translate. + */ +const ALWAYS_TRANSLATE_LANGS_PREF = + "browser.translations.alwaysTranslateLanguages"; +/** + * The list of BCP-47 language tags that will prevent showing Translations UI. + */ +const NEVER_TRANSLATE_LANGS_PREF = + "browser.translations.neverTranslateLanguages"; + +function Tree(aId, aData) { + this._data = aData; + this._tree = document.getElementById(aId); + this._tree.view = this; +} + +Tree.prototype = { + get tree() { + return this._tree; + }, + get isEmpty() { + return !this._data.length; + }, + get hasSelection() { + return this.selection.count > 0; + }, + getSelectedItems() { + let result = []; + + let rc = this.selection.getRangeCount(); + for (let i = 0; i < rc; ++i) { + let min = {}, + max = {}; + this.selection.getRangeAt(i, min, max); + for (let j = min.value; j <= max.value; ++j) { + result.push(this._data[j]); + } + } + + return result; + }, + + // nsITreeView implementation + get rowCount() { + return this._data.length; + }, + getCellText(aRow, aColumn) { + return this._data[aRow]; + }, + isSeparator(aIndex) { + return false; + }, + isSorted() { + return false; + }, + isContainer(aIndex) { + return false; + }, + setTree(aTree) {}, + getImageSrc(aRow, aColumn) {}, + getCellValue(aRow, aColumn) {}, + cycleHeader(column) {}, + getRowProperties(row) { + return ""; + }, + getColumnProperties(column) { + return ""; + }, + getCellProperties(row, column) { + return ""; + }, + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), +}; + +function Lang(aCode, label) { + this.langCode = aCode; + this._label = label; +} + +Lang.prototype = { + toString() { + return this._label; + }, +}; + +var gTranslationsSettings = { + onLoad() { + if (this._neverTranslateSiteTree) { + // Re-using an open dialog, clear the old observers. + this.removeObservers(); + } + + // Load site permissions into an array. + this._neverTranslateSites = []; + for (const perm of Services.perms.getAllByTypes([ + TRANSLATIONS_PERMISSION, + ])) { + if (perm.capability === Services.perms.DENY_ACTION) { + this._neverTranslateSites.push(perm.principal.origin); + } + } + let stripProtocol = s => s?.replace(/^\w+:/, "") || ""; + this._neverTranslateSites.sort((a, b) => { + return stripProtocol(a).localeCompare(stripProtocol(b)); + }); + + // Load language tags into arrays. + this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages(); + this._neverTranslateLangs = this.getNeverTranslateLanguages(); + + // Add observers for relevant prefs and permissions. + Services.obs.addObserver(this, "perm-changed"); + Services.prefs.addObserver(ALWAYS_TRANSLATE_LANGS_PREF, this); + Services.prefs.addObserver(NEVER_TRANSLATE_LANGS_PREF, this); + + // Build trees from the arrays. + this._alwaysTranslateLangsTree = new Tree( + "alwaysTranslateLanguagesTree", + this._alwaysTranslateLangs + ); + this._neverTranslateLangsTree = new Tree( + "neverTranslateLanguagesTree", + this._neverTranslateLangs + ); + this._neverTranslateSiteTree = new Tree( + "neverTranslateSitesTree", + this._neverTranslateSites + ); + + // Ensure the UI for each group is in the correct state. + this.onSelectAlwaysTranslateLanguage(); + this.onSelectNeverTranslateLanguage(); + this.onSelectNeverTranslateSite(); + }, + + /** + * Retrieves the value of a char-pref splits its value into an + * array delimited by commas. + * + * This is used for the translations preferences which are comma- + * separated lists of BCP-47 language tags. + * + * @param {string} pref + * @returns {Array<string>} + */ + getLangsFromPref(pref) { + let rawLangs = Services.prefs.getCharPref(pref); + if (!rawLangs) { + return []; + } + + let langArr = rawLangs.split(","); + let displayNames = Services.intl.getLanguageDisplayNames( + undefined, + langArr + ); + let langs = langArr.map((lang, i) => new Lang(lang, displayNames[i])); + langs.sort(); + + return langs; + }, + + /** + * Retrieves the always-translate language tags as an array. + * @returns {Array<string>} + */ + getAlwaysTranslateLanguages() { + return this.getLangsFromPref(ALWAYS_TRANSLATE_LANGS_PREF); + }, + + /** + * Retrieves the never-translate language tags as an array. + * @returns {Array<string>} + */ + getNeverTranslateLanguages() { + return this.getLangsFromPref(NEVER_TRANSLATE_LANGS_PREF); + }, + + /** + * Handles updating the UI components on pref or permission changes. + */ + observe(aSubject, aTopic, aData) { + if (aTopic === "perm-changed") { + if (aData === "cleared") { + // Permissions have been cleared + if (!this._neverTranslateSites.length) { + // There were no sites with permissions set, nothing to do. + return; + } + // Update the tree based on the amount of permissions removed. + let removed = this._neverTranslateSites.splice( + 0, + this._neverTranslateSites.length + ); + this._neverTranslateSiteTree.tree.rowCountChanged(0, -removed.length); + } else { + let perm = aSubject.QueryInterface(Ci.nsIPermission); + if (perm.type != TRANSLATIONS_PERMISSION) { + // The updated permission was not for Translations, nothing to do. + return; + } + if (aData === "added") { + if (perm.capability != Services.perms.DENY_ACTION) { + // We are only showing data for sites we should never translate. + // If the permission is not DENY_ACTION, we don't care about it here. + return; + } + this._neverTranslateSites.push(perm.principal.origin); + this._neverTranslateSites.sort(); + let tree = this._neverTranslateSiteTree.tree; + tree.rowCountChanged(0, 1); + tree.invalidate(); + } else if (aData == "deleted") { + let index = this._neverTranslateSites.indexOf(perm.principal.origin); + if (index == -1) { + // The deleted permission was not in the tree, nothing to do. + return; + } + this._neverTranslateSites.splice(index, 1); + this._neverTranslateSiteTree.tree.rowCountChanged(index, -1); + } + } + // Ensure the UI updates to the changes. + this.onSelectNeverTranslateSite(); + } else if (aTopic === "nsPref:changed") { + switch (aData) { + case ALWAYS_TRANSLATE_LANGS_PREF: { + this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages(); + + let alwaysTranslateLangsChange = + this._alwaysTranslateLangs.length - + this._alwaysTranslateLangsTree.rowCount; + + this._alwaysTranslateLangsTree._data = this._alwaysTranslateLangs; + let alwaysTranslateLangsTree = this._alwaysTranslateLangsTree.tree; + + if (alwaysTranslateLangsChange) { + alwaysTranslateLangsTree.rowCountChanged( + 0, + alwaysTranslateLangsChange + ); + } + + alwaysTranslateLangsTree.invalidate(); + + // Ensure the UI updates to the changes. + this.onSelectAlwaysTranslateLanguage(); + break; + } + case NEVER_TRANSLATE_LANGS_PREF: { + this._neverTranslateLangs = this.getNeverTranslateLanguages(); + + let neverTranslateLangsChange = + this._neverTranslateLangs.length - + this._neverTranslateLangsTree.rowCount; + + this._neverTranslateLangsTree._data = this._neverTranslateLangs; + let neverTranslateLangsTree = this._neverTranslateLangsTree.tree; + + if (neverTranslateLangsChange) { + neverTranslateLangsTree.rowCountChanged( + 0, + neverTranslateLangsChange + ); + } + + neverTranslateLangsTree.invalidate(); + + // Ensure the UI updates to the changes. + this.onSelectNeverTranslateLanguage(); + break; + } + } + } + }, + + /** + * Ensures that buttons states are enabled/disabled accordingly based on the + * content of the trees. + * + * The remove button should be enabled only if an item is selected. + * The removeAll button should be enabled any time the tree has content. + * + * @param {Tree} aTree + * @param {string} aIdPart + */ + _handleButtonDisabling(aTree, aIdPart) { + let empty = aTree.isEmpty; + document.getElementById("removeAll" + aIdPart + "s").disabled = empty; + document.getElementById("remove" + aIdPart).disabled = + empty || !aTree.hasSelection; + }, + + /** + * Updates the UI state for the always-translate languages section. + */ + onSelectAlwaysTranslateLanguage() { + this._handleButtonDisabling( + this._alwaysTranslateLangsTree, + "AlwaysTranslateLanguage" + ); + }, + + /** + * Updates the UI state for the never-translate languages section. + */ + onSelectNeverTranslateLanguage() { + this._handleButtonDisabling( + this._neverTranslateLangsTree, + "NeverTranslateLanguage" + ); + }, + + /** + * Updates the UI state for the never-translate sites section. + */ + onSelectNeverTranslateSite() { + this._handleButtonDisabling( + this._neverTranslateSiteTree, + "NeverTranslateSite" + ); + }, + + /** + * Updates the value of a language pref to match when a language is removed + * through the UI. + * + * @param {string} pref + * @param {Tree} tree + */ + _onRemoveLanguage(pref, tree) { + let langs = Services.prefs.getCharPref(pref); + if (!langs) { + return; + } + + let removed = tree.getSelectedItems().map(l => l.langCode); + + langs = langs.split(",").filter(l => !removed.includes(l)); + Services.prefs.setCharPref(pref, langs.join(",")); + }, + + /** + * Updates the never-translate language pref when a never-translate language + * is removed via the UI. + */ + onRemoveAlwaysTranslateLanguage() { + this._onRemoveLanguage( + ALWAYS_TRANSLATE_LANGS_PREF, + this._alwaysTranslateLangsTree + ); + }, + + /** + * Updates the always-translate language pref when a always-translate language + * is removed via the UI. + */ + onRemoveNeverTranslateLanguage() { + this._onRemoveLanguage( + NEVER_TRANSLATE_LANGS_PREF, + this._neverTranslateLangsTree + ); + }, + + /** + * Updates the permissions for a never-translate site when it is removed via the UI. + */ + onRemoveNeverTranslateSite() { + let removedNeverTranslateSites = + this._neverTranslateSiteTree.getSelectedItems(); + for (let origin of removedNeverTranslateSites) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION); + } + }, + + /** + * Clears the always-translate languages pref when the list is cleared in the UI. + */ + onRemoveAllAlwaysTranslateLanguages() { + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); + }, + + /** + * Clears the never-translate languages pref when the list is cleared in the UI. + */ + onRemoveAllNeverTranslateLanguages() { + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); + }, + + /** + * Clears the never-translate sites pref when the list is cleared in the UI. + */ + onRemoveAllNeverTranslateSites() { + if (this._neverTranslateSiteTree.isEmpty) { + return; + } + + let removedNeverTranslateSites = this._neverTranslateSites.splice( + 0, + this._neverTranslateSites.length + ); + this._neverTranslateSiteTree.tree.rowCountChanged( + 0, + -removedNeverTranslateSites.length + ); + + for (let origin of removedNeverTranslateSites) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION); + } + + this.onSelectNeverTranslateSite(); + }, + + /** + * Handles removing a selected always-translate language via the keyboard. + */ + onAlwaysTranslateLanguageKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onRemoveAlwaysTranslateLanguage(); + } + }, + + /** + * Handles removing a selected never-translate language via the keyboard. + */ + onNeverTranslateLanguageKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onRemoveNeverTranslateLanguage(); + } + }, + + /** + * Handles removing a selected never-translate site via the keyboard. + */ + onNeverTranslateSiteKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onRemoveNeverTranslateSite(); + } + }, + + /** + * Removes any active preference and permissions observers. + */ + removeObservers() { + Services.obs.removeObserver(this, "perm-changed"); + Services.prefs.removeObserver(ALWAYS_TRANSLATE_LANGS_PREF, this); + Services.prefs.removeObserver(NEVER_TRANSLATE_LANGS_PREF, this); + }, +}; diff --git a/browser/components/preferences/dialogs/translations.xhtml b/browser/components/preferences/dialogs/translations.xhtml new file mode 100644 index 0000000000..205bd54ac1 --- /dev/null +++ b/browser/components/preferences/dialogs/translations.xhtml @@ -0,0 +1,158 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window + id="TranslationsDialog" + data-l10n-id="translations-settings-title" + data-l10n-attrs="title, style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gTranslationsSettings.onLoad();" + onunload="gTranslationsSettings.removeObservers();" + persist="width height" +> + <dialog + buttons="accept" + data-l10n-id="translations-settings-close-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept" + > + <linkset> + <html:link rel="localization" href="browser/translations.ftl" /> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/translations.js" /> + + <keyset> + <key + data-l10n-id="translations-settings-close-key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane"> + <vbox flex="1"> + <label + id="alwaysTranslateLanguagesLabel" + data-l10n-id="translations-settings-always-translate-langs-description" + control="permissionsTree" + /> + <separator class="thin" /> + <tree + id="alwaysTranslateLanguagesTree" + flex="1" + style="height: 12em" + hidecolumnpicker="true" + onkeypress="gTranslationsSettings.onAlwaysTranslateLanguageKeyPress(event)" + onselect="gTranslationsSettings.onSelectAlwaysTranslateLanguage();" + > + <treecols> + <treecol + id="languageCol" + data-l10n-id="translations-settings-languages-column" + flex="1" + /> + </treecols> + <treechildren /> + </tree> + </vbox> + <hbox class="actionButtons" pack="start"> + <button + id="removeAlwaysTranslateLanguage" + disabled="true" + data-l10n-id="translations-settings-remove-language-button" + oncommand="gTranslationsSettings.onRemoveAlwaysTranslateLanguage();" + /> + <button + id="removeAllAlwaysTranslateLanguages" + data-l10n-id="translations-settings-remove-all-languages-button" + oncommand="gTranslationsSettings.onRemoveAllAlwaysTranslateLanguages();" + /> + </hbox> + <separator /> + <vbox flex="1"> + <label + id="neverTranslateLanguagesLabel" + data-l10n-id="translations-settings-never-translate-langs-description" + control="permissionsTree" + /> + <separator class="thin" /> + <tree + id="neverTranslateLanguagesTree" + flex="1" + style="height: 12em" + hidecolumnpicker="true" + onkeypress="gTranslationsSettings.onNeverTranslateLanguageKeyPress(event)" + onselect="gTranslationsSettings.onSelectNeverTranslateLanguage();" + > + <treecols> + <treecol + id="languageCol" + data-l10n-id="translations-settings-languages-column" + flex="1" + /> + </treecols> + <treechildren /> + </tree> + </vbox> + <hbox class="actionButtons" pack="start"> + <button + id="removeNeverTranslateLanguage" + disabled="true" + data-l10n-id="translations-settings-remove-language-button" + oncommand="gTranslationsSettings.onRemoveNeverTranslateLanguage();" + /> + <button + id="removeAllNeverTranslateLanguages" + data-l10n-id="translations-settings-remove-all-languages-button" + oncommand="gTranslationsSettings.onRemoveAllNeverTranslateLanguages();" + /> + </hbox> + <separator /> + <vbox flex="1"> + <label + id="neverTranslateSitesLabel" + data-l10n-id="translations-settings-never-translate-sites-description" + control="permissionsTree" + /> + <separator class="thin" /> + <tree + id="neverTranslateSitesTree" + flex="1" + style="height: 12em" + hidecolumnpicker="true" + onkeypress="gTranslationsSettings.onNeverTranslateSiteKeyPress(event)" + onselect="gTranslationsSettings.onSelectNeverTranslateSite();" + > + <treecols> + <treecol + id="siteCol" + data-l10n-id="translations-settings-sites-column" + flex="1" + /> + </treecols> + <treechildren /> + </tree> + </vbox> + <hbox class="actionButtons" pack="start"> + <button + id="removeNeverTranslateSite" + disabled="true" + data-l10n-id="translations-settings-remove-site-button" + oncommand="gTranslationsSettings.onRemoveNeverTranslateSite();" + /> + <button + id="removeAllNeverTranslateSites" + data-l10n-id="translations-settings-remove-all-sites-button" + oncommand="gTranslationsSettings.onRemoveAllNeverTranslateSites();" + /> + </hbox> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/experimental.inc.xhtml b/browser/components/preferences/experimental.inc.xhtml new file mode 100644 index 0000000000..67ba010102 --- /dev/null +++ b/browser/components/preferences/experimental.inc.xhtml @@ -0,0 +1,37 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<!-- Experimental panel --> + +<script src="chrome://browser/content/preferences/experimental.js"/> +<html:template id="template-paneExperimental"> +<vbox id="firefoxExperimentalCategory" + class="subcategory" + hidden="true" + data-category="paneExperimental"> + <html:h1 style="flex: 1;" data-l10n-id="pane-experimental-title"/> + <label><html:h2 id="pane-experimental-subtitle" data-l10n-id="pane-experimental-subtitle"/></label> + <hbox pack="end"> + <button id="experimentalCategory-reset" + class="accessory-button" + data-l10n-id="pane-experimental-reset"/> + </hbox> +</vbox> + +<groupbox data-category="paneExperimental" + id="pane-experimental-featureGates" + hidden="true"> + <label class="search-header" hidden="true"> + <html:h2 id="pane-experimental-search-results-header" data-l10n-id="pane-experimental-search-results-header"/> + </label> + <html:p data-l10n-id="pane-experimental-description2"/> +</groupbox> +</html:template> + +<html:template id="template-featureGate"> + <html:div class="featureGate"> + <checkbox class="featureGateCheckbox"/> + <label class="featureGateDescription"/> + </html:div> +</html:template> diff --git a/browser/components/preferences/experimental.js b/browser/components/preferences/experimental.js new file mode 100644 index 0000000000..266aeabc4f --- /dev/null +++ b/browser/components/preferences/experimental.js @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from preferences.js */ + +var gExperimentalPane = { + inited: false, + _template: null, + _featureGatesContainer: null, + _boundRestartObserver: null, + _observedPrefs: [], + _shouldPromptForRestart: true, + + _featureGatePrefTypeToPrefServiceType(featureGatePrefType) { + if (featureGatePrefType != "boolean") { + throw new Error("Only boolean FeatureGates are supported"); + } + return "bool"; + }, + + async _observeRestart(aSubject, aTopic, aData) { + if (!this._shouldPromptForRestart) { + return; + } + let prefValue = Services.prefs.getBoolPref(aData); + let buttonIndex = await confirmRestartPrompt(prefValue, 1, true, false); + if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) { + Services.startup.quit( + Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart + ); + return; + } + this._shouldPromptForRestart = false; + Services.prefs.setBoolPref(aData, !prefValue); + this._shouldPromptForRestart = true; + }, + + addPrefObserver(name, fn) { + this._observedPrefs.push({ name, fn }); + Services.prefs.addObserver(name, fn); + }, + + removePrefObservers() { + for (let { name, fn } of this._observedPrefs) { + Services.prefs.removeObserver(name, fn); + } + this._observedPrefs = []; + }, + + // Reset the features to their default values + async resetAllFeatures() { + let features = await gExperimentalPane.getFeatures(); + for (let feature of features) { + Services.prefs.setBoolPref(feature.preference, feature.defaultValue); + } + }, + + async getFeatures() { + let searchParams = new URLSearchParams(document.documentURIObject.query); + let definitionsUrl = searchParams.get("definitionsUrl"); + let features = await FeatureGate.all(definitionsUrl); + return features.filter(f => f.isPublic); + }, + + async _sortFeatures(features) { + // Sort the features alphabetically by their title + let titles = await document.l10n.formatMessages( + features.map(f => { + return { id: f.title }; + }) + ); + titles = titles.map((title, index) => [title.attributes[0].value, index]); + titles.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase())); + // Get the features in order of sorted titles. + return titles.map(([, index]) => features[index]); + }, + + async init() { + if (this.inited) { + return; + } + this.inited = true; + + let features = await this.getFeatures(); + let shouldHide = !features.length; + document.getElementById("category-experimental").hidden = shouldHide; + // Cache the visibility so we can show it quicker in subsequent loads. + Services.prefs.setBoolPref( + "browser.preferences.experimental.hidden", + shouldHide + ); + if (shouldHide) { + // Remove the 'experimental' category if there are no available features + document.getElementById("firefoxExperimentalCategory").remove(); + if ( + document.getElementById("categories").selectedItem?.id == + "category-experimental" + ) { + // Leave the 'experimental' category if there are no available features + gotoPref("general"); + return; + } + } + + features = await this._sortFeatures(features); + + setEventListener( + "experimentalCategory-reset", + "command", + gExperimentalPane.resetAllFeatures + ); + + window.addEventListener("unload", () => this.removePrefObservers()); + this._template = document.getElementById("template-featureGate"); + this._featureGatesContainer = document.getElementById( + "pane-experimental-featureGates" + ); + this._boundRestartObserver = this._observeRestart.bind(this); + let frag = document.createDocumentFragment(); + for (let feature of features) { + if (Preferences.get(feature.preference)) { + console.error( + "Preference control already exists for experimental feature '" + + feature.id + + "' with preference '" + + feature.preference + + "'" + ); + continue; + } + if (feature.restartRequired) { + this.addPrefObserver(feature.preference, this._boundRestartObserver); + } + let template = this._template.content.cloneNode(true); + let description = template.querySelector(".featureGateDescription"); + description.id = feature.id + "-description"; + let descriptionLinks = feature.descriptionLinks || {}; + for (let [key, value] of Object.entries(descriptionLinks)) { + let link = document.createElement("a"); + link.setAttribute("data-l10n-name", key); + link.setAttribute("href", value); + link.setAttribute("target", "_blank"); + description.append(link); + } + document.l10n.setAttributes(description, feature.description); + let checkbox = template.querySelector(".featureGateCheckbox"); + checkbox.setAttribute("preference", feature.preference); + checkbox.id = feature.id; + checkbox.setAttribute("aria-describedby", description.id); + document.l10n.setAttributes(checkbox, feature.title); + frag.appendChild(template); + let preference = Preferences.add({ + id: feature.preference, + type: gExperimentalPane._featureGatePrefTypeToPrefServiceType( + feature.type + ), + }); + preference.setElementValue(checkbox); + } + this._featureGatesContainer.appendChild(frag); + }, +}; diff --git a/browser/components/preferences/extensionControlled.js b/browser/components/preferences/extensionControlled.js new file mode 100644 index 0000000000..3c6f78ab42 --- /dev/null +++ b/browser/components/preferences/extensionControlled.js @@ -0,0 +1,309 @@ +/* - This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from preferences.js */ + +"use strict"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// Note: we get loaded in dialogs so we need to define our +// own getters, separate from preferences.js . +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + + ExtensionPreferencesManager: + "resource://gre/modules/ExtensionPreferencesManager.sys.mjs", + + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", + + Management: "resource://gre/modules/Extension.sys.mjs", +}); + +const PREF_SETTING_TYPE = "prefs"; +const PROXY_KEY = "proxy.settings"; +const API_PROXY_PREFS = [ + "network.proxy.type", + "network.proxy.http", + "network.proxy.http_port", + "network.proxy.share_proxy_settings", + "network.proxy.ssl", + "network.proxy.ssl_port", + "network.proxy.socks", + "network.proxy.socks_port", + "network.proxy.socks_version", + "network.proxy.socks_remote_dns", + "network.proxy.no_proxies_on", + "network.proxy.autoconfig_url", + "signon.autologin.proxy", +]; + +let extensionControlledContentIds = { + "privacy.containers": "browserContainersExtensionContent", + webNotificationsDisabled: "browserNotificationsPermissionExtensionContent", + "services.passwordSavingEnabled": "passwordManagerExtensionContent", + "proxy.settings": "proxyExtensionContent", + get "websites.trackingProtectionMode"() { + return { + button: "contentBlockingDisableTrackingProtectionExtension", + section: "contentBlockingTrackingProtectionExtensionContentLabel", + }; + }, +}; + +const extensionControlledL10nKeys = { + webNotificationsDisabled: "web-notifications", + "services.passwordSavingEnabled": "password-saving", + "privacy.containers": "privacy-containers", + "websites.trackingProtectionMode": "websites-content-blocking-all-trackers", + "proxy.settings": "proxy-config", +}; + +let extensionControlledIds = {}; + +/** + * Check if a pref is being managed by an extension. + */ +async function getControllingExtensionInfo(type, settingName) { + await ExtensionSettingsStore.initialize(); + return ExtensionSettingsStore.getSetting(type, settingName); +} + +function getControllingExtensionEls(settingName) { + let idInfo = extensionControlledContentIds[settingName]; + let section = document.getElementById(idInfo.section || idInfo); + let button = idInfo.button + ? document.getElementById(idInfo.button) + : section.querySelector("button"); + return { + section, + button, + description: section.querySelector("description"), + }; +} + +async function getControllingExtension(type, settingName) { + let info = await getControllingExtensionInfo(type, settingName); + let addon = info && info.id && (await AddonManager.getAddonByID(info.id)); + return addon; +} + +async function handleControllingExtension(type, settingName) { + let addon = await getControllingExtension(type, settingName); + + // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks + // an extension is controlling a setting but the extension has been uninstalled + // outside of the regular lifecycle. If the extension isn't currently installed + // then we should treat the setting as not being controlled. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example. + if (addon) { + extensionControlledIds[settingName] = addon.id; + showControllingExtension(settingName, addon); + } else { + let elements = getControllingExtensionEls(settingName); + if ( + extensionControlledIds[settingName] && + !document.hidden && + elements.button + ) { + showEnableExtensionMessage(settingName); + } else { + hideControllingExtension(settingName); + } + delete extensionControlledIds[settingName]; + } + + return !!addon; +} + +function settingNameToL10nID(settingName) { + if (!extensionControlledL10nKeys.hasOwnProperty(settingName)) { + throw new Error( + `Unknown extension controlled setting name: ${settingName}` + ); + } + return `extension-controlling-${extensionControlledL10nKeys[settingName]}`; +} + +/** + * Set the localization data for the description of the controlling extension. + * + * The function alters the inner DOM structure of the fragment to, depending + * on the `addon` argument, remove the `<img/>` element or ensure it's + * set to the correct src. + * This allows Fluent DOM Overlays to localize the fragment. + * + * @param elem {Element} + * <description> element to be annotated + * @param addon {Object?} + * Addon object with meta information about the addon (or null) + * @param settingName {String} + * If `addon` is set this handled the name of the setting that will be used + * to fetch the l10n id for the given message. + * If `addon` is set to null, this will be the full l10n-id assigned to the + * element. + */ +function setControllingExtensionDescription(elem, addon, settingName) { + const existingImg = elem.querySelector("img"); + if (addon === null) { + // If the element has an image child element, + // remove it. + if (existingImg) { + existingImg.remove(); + } + document.l10n.setAttributes(elem, settingName); + return; + } + + const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; + const src = addon.iconURL || defaultIcon; + + if (!existingImg) { + // If an element doesn't have an image child + // node, add it. + let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); + image.setAttribute("src", src); + image.setAttribute("data-l10n-name", "icon"); + image.setAttribute("role", "presentation"); + image.classList.add("extension-controlled-icon"); + elem.appendChild(image); + } else if (existingImg.getAttribute("src") !== src) { + existingImg.setAttribute("src", src); + } + + const l10nId = settingNameToL10nID(settingName); + document.l10n.setAttributes(elem, l10nId, { + name: addon.name, + }); +} + +async function showControllingExtension(settingName, addon) { + // Tell the user what extension is controlling the setting. + let elements = getControllingExtensionEls(settingName); + + elements.section.classList.remove("extension-controlled-disabled"); + let description = elements.description; + + setControllingExtensionDescription(description, addon, settingName); + + if (elements.button) { + elements.button.hidden = false; + } + + // Show the controlling extension row and hide the old label. + elements.section.hidden = false; +} + +function hideControllingExtension(settingName) { + let elements = getControllingExtensionEls(settingName); + elements.section.hidden = true; + if (elements.button) { + elements.button.hidden = true; + } +} + +function showEnableExtensionMessage(settingName) { + let elements = getControllingExtensionEls(settingName); + + elements.button.hidden = true; + elements.section.classList.add("extension-controlled-disabled"); + + elements.description.textContent = ""; + + // We replace localization of the <description> with a DOM Fragment containing + // the enable-extension-enable message. That means a change from: + // + // <description data-l10n-id="..."/> + // + // to: + // + // <description> + // <img/> + // <label data-l10n-id="..."/> + // </description> + // + // We need to remove the l10n-id annotation from the <description> to prevent + // Fluent from overwriting the element in case of any retranslation. + elements.description.removeAttribute("data-l10n-id"); + + let icon = (url, name) => { + let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); + img.src = url; + img.setAttribute("data-l10n-name", name); + img.setAttribute("role", "presentation"); + img.className = "extension-controlled-icon"; + return img; + }; + let label = document.createXULElement("label"); + let addonIcon = icon( + "chrome://mozapps/skin/extensions/extensionGeneric.svg", + "addons-icon" + ); + let toolbarIcon = icon("chrome://browser/skin/menu.svg", "menu-icon"); + label.appendChild(addonIcon); + label.appendChild(toolbarIcon); + document.l10n.setAttributes(label, "extension-controlled-enable"); + elements.description.appendChild(label); + let dismissButton = document.createXULElement("image"); + dismissButton.setAttribute("class", "extension-controlled-icon close-icon"); + dismissButton.addEventListener("click", function dismissHandler() { + hideControllingExtension(settingName); + dismissButton.removeEventListener("click", dismissHandler); + }); + elements.description.appendChild(dismissButton); +} + +function makeDisableControllingExtension(type, settingName) { + return async function disableExtension() { + let { id } = await getControllingExtensionInfo(type, settingName); + let addon = await AddonManager.getAddonByID(id); + await addon.disable(); + }; +} + +/** + * Initialize listeners though the Management API to update the UI + * when an extension is controlling a pref. + * @param {string} type + * @param {string} prefId The unique id of the setting + * @param {HTMLElement} controlledElement + */ +async function initListenersForPrefChange(type, prefId, controlledElement) { + await Management.asyncLoadSettingsModules(); + + let managementObserver = async () => { + let managementControlled = await handleControllingExtension(type, prefId); + // Enterprise policy may have locked the pref, so we need to preserve that + controlledElement.disabled = + managementControlled || Services.prefs.prefIsLocked(prefId); + }; + managementObserver(); + Management.on(`extension-setting-changed:${prefId}`, managementObserver); + + window.addEventListener("unload", () => { + Management.off(`extension-setting-changed:${prefId}`, managementObserver); + }); +} + +function initializeProxyUI(container) { + let deferredUpdate = new DeferredTask(() => { + container.updateProxySettingsUI(); + }, 10); + let proxyObserver = { + observe: (subject, topic, data) => { + if (API_PROXY_PREFS.includes(data)) { + deferredUpdate.arm(); + } + }, + }; + Services.prefs.addObserver("", proxyObserver); + window.addEventListener("unload", () => { + Services.prefs.removeObserver("", proxyObserver); + }); +} diff --git a/browser/components/preferences/findInPage.js b/browser/components/preferences/findInPage.js new file mode 100644 index 0000000000..26b6f23846 --- /dev/null +++ b/browser/components/preferences/findInPage.js @@ -0,0 +1,772 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from extensionControlled.js */ +/* import-globals-from preferences.js */ + +// A tweak to the standard <button> CE to use textContent on the <label> +// inside the button, which allows the text to be highlighted when the user +// is searching. + +const MozButton = customElements.get("button"); +class HighlightableButton extends MozButton { + static get inheritedAttributes() { + return Object.assign({}, super.inheritedAttributes, { + ".button-text": "text=label,accesskey,crop", + }); + } +} +customElements.define("highlightable-button", HighlightableButton, { + extends: "button", +}); + +var gSearchResultsPane = { + listSearchTooltips: new Set(), + listSearchMenuitemIndicators: new Set(), + searchInput: null, + // A map of DOM Elements to a string of keywords used in search + // XXX: We should invalidate this cache on `intl:app-locales-changed` + searchKeywords: new WeakMap(), + inited: false, + + // A (node -> boolean) map of subitems to be made visible or hidden. + subItems: new Map(), + + searchResultsHighlighted: false, + + init() { + if (this.inited) { + return; + } + this.inited = true; + this.searchInput = document.getElementById("searchInput"); + this.searchInput.hidden = !Services.prefs.getBoolPref( + "browser.preferences.search" + ); + + window.addEventListener("resize", () => { + this._recomputeTooltipPositions(); + }); + + if (!this.searchInput.hidden) { + this.searchInput.addEventListener("input", this); + this.searchInput.addEventListener("command", this); + window.addEventListener("DOMContentLoaded", () => { + this.searchInput.focus(); + // Initialize other panes in an idle callback. + window.requestIdleCallback(() => this.initializeCategories()); + }); + } + ensureScrollPadding(); + }, + + async handleEvent(event) { + // Ensure categories are initialized if idle callback didn't run sooo enough. + await this.initializeCategories(); + this.searchFunction(event); + }, + + /** + * This stops the search input from moving, when typing in it + * changes which items in the prefs are visible. + */ + fixInputPosition() { + let innerContainer = document.querySelector(".sticky-inner-container"); + let width = + window.windowUtils.getBoundsWithoutFlushing(innerContainer).width; + innerContainer.style.maxWidth = width + "px"; + }, + + /** + * Check that the text content contains the query string. + * + * @param String content + * the text content to be searched + * @param String query + * the query string + * @returns boolean + * true when the text content contains the query string else false + */ + queryMatchesContent(content, query) { + if (!content || !query) { + return false; + } + return content.toLowerCase().includes(query.toLowerCase()); + }, + + categoriesInitialized: false, + + /** + * Will attempt to initialize all uninitialized categories + */ + async initializeCategories() { + // Initializing all the JS for all the tabs + if (!this.categoriesInitialized) { + this.categoriesInitialized = true; + // Each element of gCategoryInits is a name + for (let [, /* name */ category] of gCategoryInits) { + if (!category.inited) { + await category.init(); + } + } + } + }, + + /** + * Finds and returns text nodes within node and all descendants + * Iterates through all the sibilings of the node object and adds the sibilings + * to an array if sibiling is a TEXT_NODE else checks the text nodes with in current node + * Source - http://stackoverflow.com/questions/10730309/find-all-text-nodes-in-html-page + * + * @param Node nodeObject + * DOM element + * @returns array of text nodes + */ + textNodeDescendants(node) { + if (!node) { + return []; + } + let all = []; + for (node = node.firstChild; node; node = node.nextSibling) { + if (node.nodeType === node.TEXT_NODE) { + all.push(node); + } else { + all = all.concat(this.textNodeDescendants(node)); + } + } + return all; + }, + + /** + * This function is used to find words contained within the text nodes. + * We pass in the textNodes because they contain the text to be highlighted. + * We pass in the nodeSizes to tell exactly where highlighting need be done. + * When creating the range for highlighting, if the nodes are section is split + * by an access key, it is important to have the size of each of the nodes summed. + * @param Array textNodes + * List of DOM elements + * @param Array nodeSizes + * Running size of text nodes. This will contain the same number of elements as textNodes. + * The first element is the size of first textNode element. + * For any nodes after, they will contain the summation of the nodes thus far in the array. + * Example: + * textNodes = [[This is ], [a], [n example]] + * nodeSizes = [[8], [9], [18]] + * This is used to determine the offset when highlighting + * @param String textSearch + * Concatination of textNodes's text content + * Example: + * textNodes = [[This is ], [a], [n example]] + * nodeSizes = "This is an example" + * This is used when executing the regular expression + * @param String searchPhrase + * word or words to search for + * @returns boolean + * Returns true when atleast one instance of search phrase is found, otherwise false + */ + highlightMatches(textNodes, nodeSizes, textSearch, searchPhrase) { + if (!searchPhrase) { + return false; + } + + let indices = []; + let i = -1; + while ((i = textSearch.indexOf(searchPhrase, i + 1)) >= 0) { + indices.push(i); + } + + // Looping through each spot the searchPhrase is found in the concatenated string + for (let startValue of indices) { + let endValue = startValue + searchPhrase.length; + let startNode = null; + let endNode = null; + let nodeStartIndex = null; + + // Determining the start and end node to highlight from + for (let index = 0; index < nodeSizes.length; index++) { + let lengthNodes = nodeSizes[index]; + // Determining the start node + if (!startNode && lengthNodes >= startValue) { + startNode = textNodes[index]; + nodeStartIndex = index; + // Calculating the offset when found query is not in the first node + if (index > 0) { + startValue -= nodeSizes[index - 1]; + } + } + // Determining the end node + if (!endNode && lengthNodes >= endValue) { + endNode = textNodes[index]; + // Calculating the offset when endNode is different from startNode + // or when endNode is not the first node + if (index != nodeStartIndex || index > 0) { + endValue -= nodeSizes[index - 1]; + } + } + } + let range = document.createRange(); + range.setStart(startNode, startValue); + range.setEnd(endNode, endValue); + this.getFindSelection(startNode.ownerGlobal).addRange(range); + + this.searchResultsHighlighted = true; + } + + return !!indices.length; + }, + + /** + * Get the selection instance from given window + * + * @param Object win + * The window object points to frame's window + */ + getFindSelection(win) { + // Yuck. See bug 138068. + let docShell = win.docShell; + + let controller = docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + + let selection = controller.getSelection( + Ci.nsISelectionController.SELECTION_FIND + ); + selection.setColors("currentColor", "#ffe900", "currentColor", "#003eaa"); + + return selection; + }, + + /** + * Shows or hides content according to search input + * + * @param String event + * to search for filted query in + */ + async searchFunction(event) { + let query = event.target.value.trim().toLowerCase(); + if (this.query == query) { + return; + } + + let firstQuery = !this.query && query; + let endQuery = !query && this.query; + let subQuery = this.query && query.includes(this.query); + this.query = query; + + // If there is a query, don't reshow the existing hidden subitems yet + // to avoid them flickering into view only to be hidden again by + // this next search. + this.removeAllSearchIndicators(window, !query.length); + + // Clear telemetry request if user types very frequently. + if (this.telemetryTimer) { + clearTimeout(this.telemetryTimer); + } + + let srHeader = document.getElementById("header-searchResults"); + let noResultsEl = document.getElementById("no-results-message"); + if (this.query) { + // If this is the first query, fix the search input in place. + if (firstQuery) { + this.fixInputPosition(); + } + // Showing the Search Results Tag + await gotoPref("paneSearchResults"); + srHeader.hidden = false; + + let resultsFound = false; + + // Building the range for highlighted areas + let rootPreferencesChildren = [ + ...document.querySelectorAll( + "#mainPrefPane > *:not([data-hidden-from-search], script, stringbundle)" + ), + ]; + + if (subQuery) { + // Since the previous query is a subset of the current query, + // there is no need to check elements that is hidden already. + rootPreferencesChildren = rootPreferencesChildren.filter( + el => !el.hidden + ); + } + + // Attach the bindings for all children if they were not already visible. + for (let child of rootPreferencesChildren) { + if (child.hidden) { + child.classList.add("visually-hidden"); + child.hidden = false; + } + } + + let ts = performance.now(); + let FRAME_THRESHOLD = 1000 / 60; + + // Showing or Hiding specific section depending on if words in query are found + for (let child of rootPreferencesChildren) { + if (performance.now() - ts > FRAME_THRESHOLD) { + // Creating tooltips for all the instances found + for (let anchorNode of this.listSearchTooltips) { + this.createSearchTooltip(anchorNode, this.query); + } + ts = await new Promise(resolve => + window.requestAnimationFrame(resolve) + ); + if (query !== this.query) { + return; + } + } + + if ( + !child.classList.contains("header") && + !child.classList.contains("subcategory") && + (await this.searchWithinNode(child, this.query)) + ) { + child.classList.remove("visually-hidden"); + + // Show the preceding search-header if one exists. + let groupbox = child.closest("groupbox"); + let groupHeader = + groupbox && groupbox.querySelector(".search-header"); + if (groupHeader) { + groupHeader.hidden = false; + } + + resultsFound = true; + } else { + child.classList.add("visually-hidden"); + } + } + + // Hide any subitems that don't match the search term and show + // only those that do. + if (this.subItems.size) { + for (let [subItem, matches] of this.subItems) { + subItem.classList.toggle("visually-hidden", !matches); + } + } + + noResultsEl.hidden = !!resultsFound; + noResultsEl.setAttribute("query", this.query); + // XXX: This is potentially racy in case where Fluent retranslates the + // message and ereases the query within. + // The feature is not yet supported, but we should fix for it before + // we enable it. See bug 1446389 for details. + let msgQueryElem = document.getElementById("sorry-message-query"); + msgQueryElem.textContent = this.query; + if (resultsFound) { + // Creating tooltips for all the instances found + for (let anchorNode of this.listSearchTooltips) { + this.createSearchTooltip(anchorNode, this.query); + } + + // Implant search telemetry probe after user stops typing for a while + if (this.query.length >= 2) { + this.telemetryTimer = setTimeout(() => { + Services.telemetry.keyedScalarAdd( + "preferences.search_query", + this.query, + 1 + ); + }, 1000); + } + } + } else { + if (endQuery) { + document + .querySelector(".sticky-inner-container") + .style.removeProperty("max-width"); + } + noResultsEl.hidden = true; + document.getElementById("sorry-message-query").textContent = ""; + // Going back to General when cleared + await gotoPref("paneGeneral"); + srHeader.hidden = true; + + // Hide some special second level headers in normal view + for (let element of document.querySelectorAll(".search-header")) { + element.hidden = true; + } + } + + window.dispatchEvent( + new CustomEvent("PreferencesSearchCompleted", { detail: query }) + ); + }, + + /** + * Finding leaf nodes and checking their content for words to search, + * It is a recursive function + * + * @param Node nodeObject + * DOM Element + * @param String searchPhrase + * @returns boolean + * Returns true when found in at least one childNode, false otherwise + */ + async searchWithinNode(nodeObject, searchPhrase) { + let matchesFound = false; + if ( + nodeObject.childElementCount == 0 || + nodeObject.tagName == "button" || + nodeObject.tagName == "label" || + nodeObject.tagName == "description" || + nodeObject.tagName == "menulist" || + nodeObject.tagName == "menuitem" + ) { + let simpleTextNodes = this.textNodeDescendants(nodeObject); + for (let node of simpleTextNodes) { + let result = this.highlightMatches( + [node], + [node.length], + node.textContent.toLowerCase(), + searchPhrase + ); + matchesFound = matchesFound || result; + } + + // Collecting data from anonymous content / label / description + let nodeSizes = []; + let allNodeText = ""; + let runningSize = 0; + + let accessKeyTextNodes = []; + + if ( + nodeObject.tagName == "label" || + nodeObject.tagName == "description" + ) { + accessKeyTextNodes.push(...simpleTextNodes); + } + + for (let node of accessKeyTextNodes) { + runningSize += node.textContent.length; + allNodeText += node.textContent; + nodeSizes.push(runningSize); + } + + // Access key are presented + let complexTextNodesResult = this.highlightMatches( + accessKeyTextNodes, + nodeSizes, + allNodeText.toLowerCase(), + searchPhrase + ); + + // Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text. + let labelResult = this.queryMatchesContent( + nodeObject.getAttribute("label"), + searchPhrase + ); + + // Searching some elements, such as xul:label, store their user-visible text in a "value" attribute. + // Value will be skipped for menuitem since value in menuitem could represent index number to distinct each item. + let valueResult = + nodeObject.tagName !== "menuitem" && nodeObject.tagName !== "radio" + ? this.queryMatchesContent( + nodeObject.getAttribute("value"), + searchPhrase + ) + : false; + + // Searching some elements, such as xul:button, buttons to open subdialogs + // using l10n ids. + let keywordsResult = + nodeObject.hasAttribute("search-l10n-ids") && + (await this.matchesSearchL10nIDs(nodeObject, searchPhrase)); + + if (!keywordsResult) { + // Searching some elements, such as xul:button, buttons to open subdialogs + // using searchkeywords attribute. + keywordsResult = + !keywordsResult && + nodeObject.hasAttribute("searchkeywords") && + this.queryMatchesContent( + nodeObject.getAttribute("searchkeywords"), + searchPhrase + ); + } + + // Creating tooltips for buttons + if ( + keywordsResult && + (nodeObject.tagName === "button" || nodeObject.tagName == "menulist") + ) { + this.listSearchTooltips.add(nodeObject); + } + + if (keywordsResult && nodeObject.tagName === "menuitem") { + nodeObject.setAttribute("indicator", "true"); + this.listSearchMenuitemIndicators.add(nodeObject); + let menulist = nodeObject.closest("menulist"); + + menulist.setAttribute("indicator", "true"); + this.listSearchMenuitemIndicators.add(menulist); + } + + if ( + (nodeObject.tagName == "menulist" || + nodeObject.tagName == "menuitem") && + (labelResult || valueResult || keywordsResult) + ) { + nodeObject.setAttribute("highlightable", "true"); + } + + matchesFound = + matchesFound || + complexTextNodesResult || + labelResult || + valueResult || + keywordsResult; + } + + // Should not search unselected child nodes of a <xul:deck> element + // except the "historyPane" <xul:deck> element. + if (nodeObject.tagName == "deck" && nodeObject.id != "historyPane") { + let index = nodeObject.selectedIndex; + if (index != -1) { + let result = await this.searchChildNodeIfVisible( + nodeObject, + index, + searchPhrase + ); + matchesFound = matchesFound || result; + } + } else { + for (let i = 0; i < nodeObject.childNodes.length; i++) { + let result = await this.searchChildNodeIfVisible( + nodeObject, + i, + searchPhrase + ); + matchesFound = matchesFound || result; + } + } + return matchesFound; + }, + + /** + * Search for a phrase within a child node if it is visible. + * + * @param Node nodeObject + * The parent DOM Element + * @param Number index + * The index for the childNode + * @param String searchPhrase + * @returns boolean + * Returns true when found the specific childNode, false otherwise + */ + async searchChildNodeIfVisible(nodeObject, index, searchPhrase) { + let result = false; + let child = nodeObject.childNodes[index]; + if ( + !child.hidden && + nodeObject.getAttribute("data-hidden-from-search") !== "true" + ) { + result = await this.searchWithinNode(child, searchPhrase); + // Creating tooltips for menulist element + if (result && nodeObject.tagName === "menulist") { + this.listSearchTooltips.add(nodeObject); + } + + // If this is a node for an experimental feature option or a Mozilla product item, + // add it to the list of subitems. The items that don't match the search term + // will be hidden. + if ( + Element.isInstance(child) && + (child.classList.contains("featureGate") || + child.classList.contains("mozilla-product-item")) + ) { + this.subItems.set(child, result); + } + } + return result; + }, + + /** + * Search for a phrase in l10n messages associated with the element. + * + * @param Node nodeObject + * The parent DOM Element + * @param String searchPhrase + * @returns boolean + * true when the text content contains the query string else false + */ + async matchesSearchL10nIDs(nodeObject, searchPhrase) { + if (!this.searchKeywords.has(nodeObject)) { + // The `search-l10n-ids` attribute is a comma-separated list of + // l10n ids. It may also uses a dot notation to specify an attribute + // of the message to be used. + // + // Example: "containers-add-button.label, user-context-personal" + // + // The result is an array of arrays of l10n ids and optionally attribute names. + // + // Example: [["containers-add-button", "label"], ["user-context-personal"]] + const refs = nodeObject + .getAttribute("search-l10n-ids") + .split(",") + .map(s => s.trim().split(".")) + .filter(s => !!s[0].length); + + const messages = await document.l10n.formatMessages( + refs.map(ref => ({ id: ref[0] })) + ); + + // Map the localized messages taking value or a selected attribute and + // building a string of concatenated translated strings out of it. + let keywords = messages + .map((msg, i) => { + let [refId, refAttr] = refs[i]; + if (!msg) { + console.error(`Missing search l10n id "${refId}"`); + return null; + } + if (refAttr) { + let attr = + msg.attributes && msg.attributes.find(a => a.name === refAttr); + if (!attr) { + console.error(`Missing search l10n id "${refId}.${refAttr}"`); + return null; + } + if (attr.value === "") { + console.error( + `Empty value added to search-l10n-ids "${refId}.${refAttr}"` + ); + } + return attr.value; + } + if (msg.value === "") { + console.error(`Empty value added to search-l10n-ids "${refId}"`); + } + return msg.value; + }) + .filter(keyword => keyword !== null) + .join(" "); + + this.searchKeywords.set(nodeObject, keywords); + return this.queryMatchesContent(keywords, searchPhrase); + } + + return this.queryMatchesContent( + this.searchKeywords.get(nodeObject), + searchPhrase + ); + }, + + /** + * Inserting a div structure infront of the DOM element matched textContent. + * Then calculation the offsets to position the tooltip in the correct place. + * + * @param Node anchorNode + * DOM Element + * @param String query + * Word or words that are being searched for + */ + createSearchTooltip(anchorNode, query) { + if (anchorNode.tooltipNode) { + return; + } + let searchTooltip = anchorNode.ownerDocument.createElement("span"); + let searchTooltipText = anchorNode.ownerDocument.createElement("span"); + searchTooltip.className = "search-tooltip"; + searchTooltipText.textContent = query; + searchTooltip.appendChild(searchTooltipText); + + // Set tooltipNode property to track corresponded tooltip node. + anchorNode.tooltipNode = searchTooltip; + anchorNode.parentElement.classList.add("search-tooltip-parent"); + anchorNode.parentElement.appendChild(searchTooltip); + + this._applyTooltipPosition( + searchTooltip, + this._computeTooltipPosition(anchorNode, searchTooltip) + ); + }, + + _recomputeTooltipPositions() { + let positions = []; + for (let anchorNode of this.listSearchTooltips) { + let searchTooltip = anchorNode.tooltipNode; + if (!searchTooltip) { + continue; + } + let position = this._computeTooltipPosition(anchorNode, searchTooltip); + positions.push({ searchTooltip, position }); + } + for (let { searchTooltip, position } of positions) { + this._applyTooltipPosition(searchTooltip, position); + } + }, + + _applyTooltipPosition(searchTooltip, position) { + searchTooltip.style.left = position.left + "px"; + searchTooltip.style.top = position.top + "px"; + }, + + _computeTooltipPosition(anchorNode, searchTooltip) { + // In order to get the up-to-date position of each of the nodes that we're + // putting tooltips on, we have to flush layout intentionally. Once + // menulists don't use XUL layout we can remove this and use plain CSS to + // position them, see bug 1363730. + let anchorRect = anchorNode.getBoundingClientRect(); + let containerRect = anchorNode.parentElement.getBoundingClientRect(); + let tooltipRect = searchTooltip.getBoundingClientRect(); + + let left = + anchorRect.left - + containerRect.left + + anchorRect.width / 2 - + tooltipRect.width / 2; + let top = anchorRect.top - containerRect.top; + return { left, top }; + }, + + /** + * Remove all search indicators. This would be called when switching away from + * a search to another preference category. + */ + removeAllSearchIndicators(window, showSubItems) { + if (this.searchResultsHighlighted) { + this.getFindSelection(window).removeAllRanges(); + this.searchResultsHighlighted = false; + } + this.removeAllSearchTooltips(); + this.removeAllSearchMenuitemIndicators(); + + // Make any previously hidden subitems visible again for the next search. + if (showSubItems && this.subItems.size) { + for (let subItem of this.subItems.keys()) { + subItem.classList.remove("visually-hidden"); + } + this.subItems.clear(); + } + }, + + /** + * Remove all search tooltips. + */ + removeAllSearchTooltips() { + for (let anchorNode of this.listSearchTooltips) { + anchorNode.parentElement.classList.remove("search-tooltip-parent"); + if (anchorNode.tooltipNode) { + anchorNode.tooltipNode.remove(); + } + anchorNode.tooltipNode = null; + } + this.listSearchTooltips.clear(); + }, + + /** + * Remove all indicators on menuitem. + */ + removeAllSearchMenuitemIndicators() { + for (let node of this.listSearchMenuitemIndicators) { + node.removeAttribute("indicator"); + } + this.listSearchMenuitemIndicators.clear(); + }, +}; diff --git a/browser/components/preferences/fxaPairDevice.js b/browser/components/preferences/fxaPairDevice.js new file mode 100644 index 0000000000..8f5c26734d --- /dev/null +++ b/browser/components/preferences/fxaPairDevice.js @@ -0,0 +1,144 @@ +// 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { Weave } = ChromeUtils.importESModule( + "resource://services-sync/main.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs", +}); + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const QR = require("devtools/shared/qrcode/index"); + +// This is only for "labor illusion", see +// https://www.fastcompany.com/3061519/the-ux-secret-that-will-ruin-apps-for-you +const MIN_PAIRING_LOADING_TIME_MS = 1000; + +/** + * Communication between FxAccountsPairingFlow and gFxaPairDeviceDialog + * is done using an emitter via the following messages: + * <- [view:SwitchToWebContent] - Notifies the view to navigate to a specific URL. + * <- [view:Error] - Notifies the view something went wrong during the pairing process. + * -> [view:Closed] - Notifies the pairing module the view was closed. + */ +var gFxaPairDeviceDialog = { + init() { + this._resetBackgroundQR(); + // We let the modal show itself before eventually showing a primary-password dialog later. + Services.tm.dispatchToMainThread(() => this.startPairingFlow()); + }, + + uninit() { + // When the modal closes we want to remove any query params + // To prevent refreshes/restores from reopening the dialog + const browser = window.docShell.chromeEventHandler; + browser.loadURI(Services.io.newURI("about:preferences#sync"), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + + this.teardownListeners(); + this._emitter.emit("view:Closed"); + }, + + async startPairingFlow() { + this._resetBackgroundQR(); + document + .getElementById("qrWrapper") + .setAttribute("pairing-status", "loading"); + this._emitter = new EventEmitter(); + this.setupListeners(); + try { + if (!Weave.Utils.ensureMPUnlocked()) { + throw new Error("Master-password locked."); + } + // To keep consistent with our accounts.firefox.com counterpart + // we restyle the parent dialog this is contained in + this._styleParentDialog(); + + const [, uri] = await Promise.all([ + new Promise(res => setTimeout(res, MIN_PAIRING_LOADING_TIME_MS)), + FxAccountsPairingFlow.start({ emitter: this._emitter }), + ]); + const imgData = QR.encodeToDataURI(uri, "L"); + document.getElementById( + "qrContainer" + ).style.backgroundImage = `url("${imgData.src}")`; + document + .getElementById("qrWrapper") + .setAttribute("pairing-status", "ready"); + } catch (e) { + this.onError(e); + } + }, + + _styleParentDialog() { + // Since the dialog title is in the above document, we can't query the + // document in this level and need to go up one + let dialogParent = window.parent.document; + + // To allow the firefox icon to go over the dialog + let dialogBox = dialogParent.querySelector(".dialogBox"); + dialogBox.style.overflow = "visible"; + dialogBox.style.borderRadius = "12px"; + + let dialogTitle = dialogParent.querySelector(".dialogTitleBar"); + dialogTitle.style.borderBottom = "none"; + dialogTitle.classList.add("fxaPairDeviceIcon"); + }, + + _resetBackgroundQR() { + // The text we encode doesn't really matter as it is un-scannable (blurry and very transparent). + const imgData = QR.encodeToDataURI( + "https://accounts.firefox.com/pair", + "L" + ); + document.getElementById( + "qrContainer" + ).style.backgroundImage = `url("${imgData.src}")`; + }, + + onError(err) { + console.error(err); + this.teardownListeners(); + document + .getElementById("qrWrapper") + .setAttribute("pairing-status", "error"); + }, + + _switchToUrl(url) { + const browser = window.docShell.chromeEventHandler; + browser.fixupAndLoadURIString(url, { + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( + {} + ), + }); + }, + + setupListeners() { + this._switchToWebContent = (_, url) => this._switchToUrl(url); + this._onError = (_, error) => this.onError(error); + this._emitter.once("view:SwitchToWebContent", this._switchToWebContent); + this._emitter.on("view:Error", this._onError); + }, + + teardownListeners() { + try { + this._emitter.off("view:SwitchToWebContent", this._switchToWebContent); + this._emitter.off("view:Error", this._onError); + } catch (e) { + console.warn("Error while tearing down listeners.", e); + } + }, +}; diff --git a/browser/components/preferences/fxaPairDevice.xhtml b/browser/components/preferences/fxaPairDevice.xhtml new file mode 100644 index 0000000000..8b66cbe00e --- /dev/null +++ b/browser/components/preferences/fxaPairDevice.xhtml @@ -0,0 +1,75 @@ +<?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/fxaPairDevice.css" type="text/css"?> + +<window + id="fxaPairDeviceDialog" + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gFxaPairDeviceDialog.init();" + onunload="gFxaPairDeviceDialog.uninit()" + data-l10n-id="fxa-pair-device-dialog-sync2" + data-l10n-attrs="style" +> + <dialog id="fxaPairDeviceDialog1" buttons="accept"> + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="browser/preferences/fxaPairDevice.ftl" + /> + <html:link rel="localization" href="toolkit/branding/accounts.ftl" /> + </linkset> + <script src="chrome://browser/content/preferences/fxaPairDevice.js" /> + + <description id="pairTitle" data-l10n-id="fxa-qrcode-pair-title"> + </description> + <vbox id="qrCodeDisplay"> + <description class="pairHeading" data-l10n-id="fxa-qrcode-pair-step1"> + </description> + <description + class="pairHeading" + data-l10n-id="fxa-qrcode-pair-step2-signin" + > + <html:img + src="chrome://browser/skin/preferences/ios-menu.svg" + data-l10n-name="ios-menu-icon" + class="menu-icon" + /> + <html:img + src="chrome://browser/skin/preferences/android-menu.svg" + data-l10n-name="android-menu-icon" + class="menu-icon" + /> + </description> + <description + class="pairHeading" + data-l10n-id="fxa-qrcode-pair-step3" + ></description> + <vbox> + <vbox align="center" id="qrWrapper" pairing-status="loading"> + <box id="qrContainer"></box> + <box id="qrSpinner"></box> + <vbox id="qrError" onclick="gFxaPairDeviceDialog.startPairingFlow();"> + <image id="refresh-qr" /> + <label + class="qr-error-text" + data-l10n-id="fxa-qrcode-error-title" + ></label> + <label + class="qr-error-text" + data-l10n-id="fxa-qrcode-error-body" + ></label> + </vbox> + </vbox> + </vbox> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml new file mode 100644 index 0000000000..9e6121e28d --- /dev/null +++ b/browser/components/preferences/home.inc.xhtml @@ -0,0 +1,92 @@ +# 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/. + +<!-- Home panel --> + +<script src="chrome://browser/content/preferences/home.js"/> +<html:template id="template-paneHome"> +<hbox id="firefoxHomeCategory" + class="subcategory" + hidden="true" + data-category="paneHome"> + <html:h1 style="flex: 1;" data-l10n-id="pane-home-title"/> + <button id="restoreDefaultHomePageBtn" + is="highlightable-button" + class="homepage-button check-home-page-controlled" + data-preference-related="browser.startup.homepage" + data-l10n-id="home-restore-defaults" + preference="pref.browser.homepage.disable_button.restore_default"/> +</hbox> + +<groupbox id="homepageGroup" + data-category="paneHome" + hidden="true"> + <label><html:h2 data-l10n-id="home-new-windows-tabs-header"/></label> + <description data-l10n-id="home-new-windows-tabs-description2" /> + + <hbox id="homepageAndNewWindowsOption" align="center" data-subcategory="homeOverride"> + <label control="homeMode" data-l10n-id="home-homepage-mode-label" flex="1" /> + + <vbox flex="1"> + <menulist id="homeMode" + class="check-home-page-controlled" + data-preference-related="browser.startup.homepage"> + <menupopup> + <menuitem value="0" data-l10n-id="home-mode-choice-default-fx" /> + <menuitem value="2" data-l10n-id="home-mode-choice-custom" /> + <menuitem value="1" data-l10n-id="home-mode-choice-blank" /> + </menupopup> + </menulist> + + <vbox id="customSettings" hidden="true"> + <box role="combobox"> + <html:input id="homePageUrl" + type="text" + is="autocomplete-input" + class="uri-element check-home-page-controlled" + style="flex: 1;" + data-preference-related="browser.startup.homepage" + data-l10n-id="home-homepage-custom-url" + autocompletepopup="homePageUrlAutocomplete" /> + <popupset> + <panel id="homePageUrlAutocomplete" + is="autocomplete-richlistbox-popup" + type="autocomplete-richlistbox" + noautofocus="true"/> + </popupset> + </box> + <hbox class="homepage-buttons"> + <button id="useCurrentBtn" + is="highlightable-button" + class="homepage-button check-home-page-controlled" + data-l10n-id="use-current-pages" + data-l10n-args='{"tabCount": 0}' + disabled="true" + preference="pref.browser.homepage.disable_button.current_page"/> + <button id="useBookmarkBtn" + is="highlightable-button" + class="homepage-button check-home-page-controlled" + data-l10n-id="choose-bookmark" + preference="pref.browser.homepage.disable_button.bookmark_page" + search-l10n-ids="select-bookmark-window2.title, select-bookmark-desc"/> + </hbox> + </vbox> + </vbox> + </hbox> + <hbox id="newTabsOption" data-subcategory="newtabOverride" align="center"> + <label control="newTabMode" data-l10n-id="home-newtabs-mode-label" flex="1" /> + + <vbox flex="1"> + <!-- This can be set to an extension value which is managed outside of + Preferences so we need to handle setting the pref manually.--> + <menulist id="newTabMode" flex="1" data-preference-related="browser.newtabpage.enabled"> + <menupopup> + <menuitem value="0" data-l10n-id="home-mode-choice-default-fx" /> + <menuitem value="1" data-l10n-id="home-mode-choice-blank" /> + </menupopup> + </menulist> + </vbox> + </hbox> +</groupbox> +</html:template> diff --git a/browser/components/preferences/home.js b/browser/components/preferences/home.js new file mode 100644 index 0000000000..6aa72b84b8 --- /dev/null +++ b/browser/components/preferences/home.js @@ -0,0 +1,694 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from extensionControlled.js */ +/* import-globals-from preferences.js */ +/* import-globals-from main.js */ + +// HOME PAGE + +/* + * Preferences: + * + * browser.startup.homepage + * - the user's home page, as a string; if the home page is a set of tabs, + * this will be those URLs separated by the pipe character "|" + * browser.newtabpage.enabled + * - determines that is shown on the user's new tab page. + * true = Activity Stream is shown, + * false = about:blank is shown + */ + +Preferences.addAll([ + { id: "browser.startup.homepage", type: "wstring" }, + { id: "pref.browser.homepage.disable_button.current_page", type: "bool" }, + { id: "pref.browser.homepage.disable_button.bookmark_page", type: "bool" }, + { id: "pref.browser.homepage.disable_button.restore_default", type: "bool" }, + { id: "browser.newtabpage.enabled", type: "bool" }, +]); + +const HOMEPAGE_OVERRIDE_KEY = "homepage_override"; +const URL_OVERRIDES_TYPE = "url_overrides"; +const NEW_TAB_KEY = "newTabURL"; + +const BLANK_HOMEPAGE_URL = "chrome://browser/content/blanktab.html"; + +var gHomePane = { + HOME_MODE_FIREFOX_HOME: "0", + HOME_MODE_BLANK: "1", + HOME_MODE_CUSTOM: "2", + HOMEPAGE_PREF: "browser.startup.homepage", + NEWTAB_ENABLED_PREF: "browser.newtabpage.enabled", + ACTIVITY_STREAM_PREF_BRANCH: "browser.newtabpage.activity-stream.", + + get homePanePrefs() { + return Preferences.getAll().filter(pref => + pref.id.includes(this.ACTIVITY_STREAM_PREF_BRANCH) + ); + }, + + get isPocketNewtabEnabled() { + const value = Services.prefs.getStringPref( + "browser.newtabpage.activity-stream.discoverystream.config", + "" + ); + if (value) { + try { + return JSON.parse(value).enabled; + } catch (e) { + console.error("Failed to parse Discovery Stream pref."); + } + } + + return false; + }, + + async syncToNewTabPref() { + let menulist = document.getElementById("newTabMode"); + + if (["0", "1"].includes(menulist.value)) { + let newtabEnabledPref = Services.prefs.getBoolPref( + this.NEWTAB_ENABLED_PREF, + true + ); + let newValue = menulist.value !== this.HOME_MODE_BLANK; + // Only set this if the pref has changed, otherwise the pref change will trigger other listeners to repeat. + if (newtabEnabledPref !== newValue) { + Services.prefs.setBoolPref(this.NEWTAB_ENABLED_PREF, newValue); + } + let selectedAddon = ExtensionSettingsStore.getSetting( + URL_OVERRIDES_TYPE, + NEW_TAB_KEY + ); + if (selectedAddon) { + ExtensionSettingsStore.select(null, URL_OVERRIDES_TYPE, NEW_TAB_KEY); + } + } else { + let addon = await AddonManager.getAddonByID(menulist.value); + if (addon && addon.isActive) { + ExtensionSettingsStore.select( + addon.id, + URL_OVERRIDES_TYPE, + NEW_TAB_KEY + ); + } + } + }, + + async syncFromNewTabPref() { + let menulist = document.getElementById("newTabMode"); + + // If the new tab url was changed to about:blank or about:newtab + if ( + AboutNewTab.newTabURL === "about:newtab" || + AboutNewTab.newTabURL === "about:blank" || + AboutNewTab.newTabURL === BLANK_HOMEPAGE_URL + ) { + let newtabEnabledPref = Services.prefs.getBoolPref( + this.NEWTAB_ENABLED_PREF, + true + ); + let newValue = newtabEnabledPref + ? this.HOME_MODE_FIREFOX_HOME + : this.HOME_MODE_BLANK; + if (newValue !== menulist.value) { + menulist.value = newValue; + } + menulist.disabled = Preferences.get(this.NEWTAB_ENABLED_PREF).locked; + // If change was triggered by installing an addon we need to update + // the value of the menulist to be that addon. + } else { + let selectedAddon = ExtensionSettingsStore.getSetting( + URL_OVERRIDES_TYPE, + NEW_TAB_KEY + ); + if (selectedAddon && menulist.value !== selectedAddon.id) { + menulist.value = selectedAddon.id; + } + } + }, + + /** + * _updateMenuInterface: adds items to or removes them from the menulists + * @param {string} selectId Optional Id of the menulist to add or remove items from. + * If not included this will update both home and newtab menus. + */ + async _updateMenuInterface(selectId) { + let selects; + if (selectId) { + selects = [document.getElementById(selectId)]; + } else { + let newTabSelect = document.getElementById("newTabMode"); + let homeSelect = document.getElementById("homeMode"); + selects = [homeSelect, newTabSelect]; + } + + for (let select of selects) { + // Remove addons from the menu popup which are no longer installed, or disabled. + // let menuOptions = select.menupopup.childNodes; + let menuOptions = Array.from(select.menupopup.childNodes); + + for (let option of menuOptions) { + // If the value is not a number, assume it is an addon ID + if (!/^\d+$/.test(option.value)) { + let addon = await AddonManager.getAddonByID(option.value); + if (option && (!addon || !addon.isActive)) { + option.remove(); + } + } + } + + let extensionOptions; + if (select.id === "homeMode") { + extensionOptions = await ExtensionSettingsStore.getAllSettings( + PREF_SETTING_TYPE, + HOMEPAGE_OVERRIDE_KEY + ); + } else { + extensionOptions = await ExtensionSettingsStore.getAllSettings( + URL_OVERRIDES_TYPE, + NEW_TAB_KEY + ); + } + let addons = await AddonManager.getAddonsByIDs( + extensionOptions.map(a => a.id) + ); + + // Add addon options to the menu popups + let menupopup = select.querySelector("menupopup"); + for (let addon of addons) { + if (!addon || !addon.id || !addon.isActive) { + continue; + } + let currentOption = select.querySelector( + `[value="${CSS.escape(addon.id)}"]` + ); + if (!currentOption) { + let option = document.createXULElement("menuitem"); + option.classList.add("addon-with-favicon"); + option.value = addon.id; + option.label = addon.name; + menupopup.append(option); + option.querySelector("image").src = addon.iconURL; + } + let setting = extensionOptions.find(o => o.id == addon.id); + if ( + (select.id === "homeMode" && setting.value == HomePage.get()) || + (select.id === "newTabMode" && setting.value == AboutNewTab.newTabURL) + ) { + select.value = addon.id; + } + } + } + }, + + /** + * watchNewTab: Listen for changes to the new tab url and enable/disable appropriate + * areas of the UI. + */ + watchNewTab() { + let newTabObserver = () => { + this.syncFromNewTabPref(); + this._updateMenuInterface("newTabMode"); + }; + Services.obs.addObserver(newTabObserver, "newtab-url-changed"); + window.addEventListener("unload", () => { + Services.obs.removeObserver(newTabObserver, "newtab-url-changed"); + }); + }, + + /** + * watchHomePrefChange: Listen for preferences changes on the Home Tab in order to + * show the appropriate home menu selection. + */ + watchHomePrefChange() { + const homePrefObserver = (subject, topic, data) => { + // only update this UI if it is exactly the HOMEPAGE_PREF, not other prefs with the same root. + if (data && data != this.HOMEPAGE_PREF) { + return; + } + this._updateUseCurrentButton(); + this._renderCustomSettings(); + this._handleHomePageOverrides(); + this._updateMenuInterface("homeMode"); + }; + + Services.prefs.addObserver(this.HOMEPAGE_PREF, homePrefObserver); + window.addEventListener("unload", () => { + Services.prefs.removeObserver(this.HOMEPAGE_PREF, homePrefObserver); + }); + }, + + /** + * Listen extension changes on the New Tab and Home Tab + * in order to update the UI and show or hide the Restore Defaults button. + */ + watchExtensionPrefChange() { + const extensionSettingChanged = (evt, setting) => { + if (setting.key == "homepage_override" && setting.type == "prefs") { + this._updateMenuInterface("homeMode"); + } else if ( + setting.key == "newTabURL" && + setting.type == "url_overrides" + ) { + this._updateMenuInterface("newTabMode"); + } + }; + + Management.on("extension-setting-changed", extensionSettingChanged); + window.addEventListener("unload", () => { + Management.off("extension-setting-changed", extensionSettingChanged); + }); + }, + + /** + * Listen for all preferences changes on the Home Tab in order to show or + * hide the Restore Defaults button. + */ + watchHomeTabPrefChange() { + const observer = () => this.toggleRestoreDefaultsBtn(); + Services.prefs.addObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer); + Services.prefs.addObserver(this.HOMEPAGE_PREF, observer); + Services.prefs.addObserver(this.NEWTAB_ENABLED_PREF, observer); + + window.addEventListener("unload", () => { + Services.prefs.removeObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer); + Services.prefs.removeObserver(this.HOMEPAGE_PREF, observer); + Services.prefs.removeObserver(this.NEWTAB_ENABLED_PREF, observer); + }); + }, + + /** + * _renderCustomSettings: Hides or shows the UI for setting a custom + * homepage URL + * @param {obj} options + * @param {bool} options.shouldShow Should the custom UI be shown? + * @param {bool} options.isControlled Is an extension controlling the home page? + */ + _renderCustomSettings(options = {}) { + let { shouldShow, isControlled } = options; + const customSettingsContainerEl = document.getElementById("customSettings"); + const customUrlEl = document.getElementById("homePageUrl"); + const homePage = HomePage.get(); + const isHomePageCustom = + (!this._isHomePageDefaultValue() && + !this.isHomePageBlank() && + !isControlled) || + homePage.locked; + + if (typeof shouldShow === "undefined") { + shouldShow = isHomePageCustom; + } + customSettingsContainerEl.hidden = !shouldShow; + + // We can't use isHomePageDefaultValue and isHomePageBlank here because we want to disregard the blank + // possibility triggered by the browser.startup.page being 0. + // We also skip when HomePage is locked because it might be locked to a default that isn't "about:home" + // (and it makes existing tests happy). + let newValue; + if ( + this._isBlankPage(homePage) || + (HomePage.isDefault && !HomePage.locked) + ) { + newValue = ""; + } else { + newValue = homePage; + } + if (customUrlEl.value !== newValue) { + customUrlEl.value = newValue; + } + }, + + /** + * _isHomePageDefaultValue + * @returns {bool} Is the homepage set to the default pref value? + */ + _isHomePageDefaultValue() { + const startupPref = Preferences.get("browser.startup.page"); + return ( + startupPref.value !== gMainPane.STARTUP_PREF_BLANK && HomePage.isDefault + ); + }, + + /** + * isHomePageBlank + * @returns {bool} Is the homepage set to about:blank? + */ + isHomePageBlank() { + const startupPref = Preferences.get("browser.startup.page"); + return ( + ["about:blank", BLANK_HOMEPAGE_URL, ""].includes(HomePage.get()) || + startupPref.value === gMainPane.STARTUP_PREF_BLANK + ); + }, + + /** + * _isTabAboutPreferences: Is a given tab set to about:preferences? + * @param {Element} aTab A tab element + * @returns {bool} Is the linkedBrowser of aElement set to about:preferences? + */ + _isTabAboutPreferences(aTab) { + return aTab.linkedBrowser.currentURI.spec.startsWith("about:preferences"); + }, + + /** + * _getTabsForHomePage + * @returns {Array} An array of current tabs + */ + _getTabsForHomePage() { + let tabs = []; + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // We should only include visible & non-pinned tabs + if ( + win && + win.document.documentElement.getAttribute("windowtype") === + "navigator:browser" + ) { + tabs = win.gBrowser.visibleTabs.slice(win.gBrowser._numPinnedTabs); + tabs = tabs.filter(tab => !this._isTabAboutPreferences(tab)); + // XXX: Bug 1441637 - Fix tabbrowser to report tab.closing before it blurs it + tabs = tabs.filter(tab => !tab.closing); + } + + return tabs; + }, + + _renderHomepageMode(controllingExtension) { + const isDefault = this._isHomePageDefaultValue(); + const isBlank = this.isHomePageBlank(); + const el = document.getElementById("homeMode"); + let newValue; + + if (controllingExtension && controllingExtension.id) { + newValue = controllingExtension.id; + } else if (isDefault) { + newValue = this.HOME_MODE_FIREFOX_HOME; + } else if (isBlank) { + newValue = this.HOME_MODE_BLANK; + } else { + newValue = this.HOME_MODE_CUSTOM; + } + if (el.value !== newValue) { + el.value = newValue; + } + }, + + _setInputDisabledStates(isControlled) { + let tabCount = this._getTabsForHomePage().length; + + // Disable or enable the inputs based on if this is controlled by an extension. + document + .querySelectorAll(".check-home-page-controlled") + .forEach(element => { + let isDisabled; + let pref = + element.getAttribute("preference") || + element.getAttribute("data-preference-related"); + if (!pref) { + throw new Error( + `Element with id ${element.id} did not have preference or data-preference-related attribute defined.` + ); + } + + if (pref === this.HOMEPAGE_PREF) { + isDisabled = HomePage.locked; + } else { + isDisabled = Preferences.get(pref).locked || isControlled; + } + + if (pref === "pref.browser.disable_button.current_page") { + // Special case for current_page to disable it if tabCount is 0 + isDisabled = isDisabled || tabCount < 1; + } + + element.disabled = isDisabled; + }); + }, + + async _handleHomePageOverrides() { + let controllingExtension; + if (HomePage.locked) { + // Disable inputs if they are locked. + this._renderCustomSettings(); + this._setInputDisabledStates(false); + } else { + if (HomePage.get().startsWith("moz-extension:")) { + controllingExtension = await getControllingExtension( + PREF_SETTING_TYPE, + HOMEPAGE_OVERRIDE_KEY + ); + } + this._setInputDisabledStates(); + this._renderCustomSettings({ + isControlled: !!controllingExtension, + }); + } + this._renderHomepageMode(controllingExtension); + }, + + onMenuChange(event) { + const { value } = event.target; + const startupPref = Preferences.get("browser.startup.page"); + let selectedAddon = ExtensionSettingsStore.getSetting( + PREF_SETTING_TYPE, + HOMEPAGE_OVERRIDE_KEY + ); + + switch (value) { + case this.HOME_MODE_FIREFOX_HOME: + if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) { + startupPref.value = gMainPane.STARTUP_PREF_HOMEPAGE; + } + if (!HomePage.isDefault) { + HomePage.reset(); + } else { + this._renderCustomSettings({ shouldShow: false }); + } + if (selectedAddon) { + ExtensionSettingsStore.select( + null, + PREF_SETTING_TYPE, + HOMEPAGE_OVERRIDE_KEY + ); + } + break; + case this.HOME_MODE_BLANK: + if (!this._isBlankPage(HomePage.get())) { + HomePage.safeSet(BLANK_HOMEPAGE_URL); + } else { + this._renderCustomSettings({ shouldShow: false }); + } + if (selectedAddon) { + ExtensionSettingsStore.select( + null, + PREF_SETTING_TYPE, + HOMEPAGE_OVERRIDE_KEY + ); + } + break; + case this.HOME_MODE_CUSTOM: + if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) { + Services.prefs.clearUserPref(startupPref.id); + } + if (HomePage.getDefault() != HomePage.getOriginalDefault()) { + HomePage.clear(); + } + this._renderCustomSettings({ shouldShow: true }); + if (selectedAddon) { + ExtensionSettingsStore.select( + null, + PREF_SETTING_TYPE, + HOMEPAGE_OVERRIDE_KEY + ); + } + break; + // extensions will have a variety of values as their ID, so treat it as default + default: + AddonManager.getAddonByID(value).then(addon => { + if (addon && addon.isActive) { + ExtensionPreferencesManager.selectSetting( + addon.id, + HOMEPAGE_OVERRIDE_KEY + ); + } + this._renderCustomSettings({ shouldShow: false }); + }); + } + }, + + /** + * Switches the "Use Current Page" button between its singular and plural + * forms. + */ + async _updateUseCurrentButton() { + let useCurrent = document.getElementById("useCurrentBtn"); + let tabs = this._getTabsForHomePage(); + const tabCount = tabs.length; + document.l10n.setAttributes(useCurrent, "use-current-pages", { tabCount }); + + // If the homepage is controlled by an extension then you can't use this. + if ( + await getControllingExtensionInfo( + PREF_SETTING_TYPE, + HOMEPAGE_OVERRIDE_KEY + ) + ) { + return; + } + + // In this case, the button's disabled state is set by preferences.xml. + let prefName = "pref.browser.homepage.disable_button.current_page"; + if (Preferences.get(prefName).locked) { + return; + } + + useCurrent.disabled = tabCount < 1; + }, + + /** + * Sets the home page to the URL(s) of any currently opened tab(s), + * updating about:preferences#home UI to reflect this. + */ + setHomePageToCurrent() { + let tabs = this._getTabsForHomePage(); + function getTabURI(t) { + return t.linkedBrowser.currentURI.spec; + } + + // FIXME Bug 244192: using dangerous "|" joiner! + if (tabs.length) { + HomePage.set(tabs.map(getTabURI).join("|")).catch(console.error); + } + }, + + _setHomePageToBookmarkClosed(rv, aEvent) { + if (aEvent.detail.button != "accept") { + return; + } + if (rv.urls && rv.names) { + // XXX still using dangerous "|" joiner! + HomePage.set(rv.urls.join("|")).catch(console.error); + } + }, + + /** + * Displays a dialog in which the user can select a bookmark to use as home + * page. If the user selects a bookmark, that bookmark's name is displayed in + * UI and the bookmark's address is stored to the home page preference. + */ + setHomePageToBookmark() { + const rv = { urls: null, names: null }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml", + { + features: "resizable=yes, modal=yes", + closingCallback: this._setHomePageToBookmarkClosed.bind(this, rv), + }, + rv + ); + }, + + restoreDefaultHomePage() { + HomePage.reset(); + this._handleHomePageOverrides(); + Services.prefs.clearUserPref(this.NEWTAB_ENABLED_PREF); + AboutNewTab.resetNewTabURL(); + }, + + onCustomHomePageChange(event) { + const value = event.target.value || HomePage.getDefault(); + HomePage.set(value).catch(console.error); + }, + + /** + * Check all Home Tab preferences for user set values. + */ + _changedHomeTabDefaultPrefs() { + // If Discovery Stream is enabled Firefox Home Content preference options are hidden + const homeContentChanged = + !this.isPocketNewtabEnabled && + this.homePanePrefs.some(pref => pref.hasUserValue); + const newtabPref = Preferences.get(this.NEWTAB_ENABLED_PREF); + const extensionControlled = Preferences.get( + "browser.startup.homepage_override.extensionControlled" + ); + + return ( + homeContentChanged || + HomePage.overridden || + newtabPref.hasUserValue || + AboutNewTab.newTabURLOverridden || + extensionControlled + ); + }, + + _isBlankPage(url) { + return url == "about:blank" || url == BLANK_HOMEPAGE_URL; + }, + + /** + * Show the Restore Defaults button if any preference on the Home tab was + * changed, or hide it otherwise. + */ + toggleRestoreDefaultsBtn() { + const btn = document.getElementById("restoreDefaultHomePageBtn"); + const prefChanged = this._changedHomeTabDefaultPrefs(); + if (prefChanged) { + btn.style.removeProperty("visibility"); + } else { + btn.style.visibility = "hidden"; + } + }, + + /** + * Set all prefs on the Home tab back to their default values. + */ + restoreDefaultPrefsForHome() { + this.restoreDefaultHomePage(); + // If Discovery Stream is enabled Firefox Home Content preference options are hidden + if (!this.isPocketNewtabEnabled) { + this.homePanePrefs.forEach(pref => Services.prefs.clearUserPref(pref.id)); + } + }, + + init() { + // Event Listeners + document + .getElementById("homePageUrl") + .addEventListener("change", this.onCustomHomePageChange.bind(this)); + document + .getElementById("useCurrentBtn") + .addEventListener("command", this.setHomePageToCurrent.bind(this)); + document + .getElementById("useBookmarkBtn") + .addEventListener("command", this.setHomePageToBookmark.bind(this)); + document + .getElementById("restoreDefaultHomePageBtn") + .addEventListener("command", this.restoreDefaultPrefsForHome.bind(this)); + + // Setup the add-on options for the new tab section before registering the + // listener. + this._updateMenuInterface(); + document + .getElementById("newTabMode") + .addEventListener("command", this.syncToNewTabPref.bind(this)); + document + .getElementById("homeMode") + .addEventListener("command", this.onMenuChange.bind(this)); + + this._updateUseCurrentButton(); + this._handleHomePageOverrides(); + this.syncFromNewTabPref(); + window.addEventListener("focus", this._updateUseCurrentButton.bind(this)); + + // Extension/override-related events + this.watchNewTab(); + this.watchHomePrefChange(); + this.watchExtensionPrefChange(); + this.watchHomeTabPrefChange(); + // Notify observers that the UI is now ready + Services.obs.notifyObservers(window, "home-pane-loaded"); + }, +}; diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn new file mode 100644 index 0000000000..2131a15cee --- /dev/null +++ b/browser/components/preferences/jar.mn @@ -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/. + +browser.jar: + content/browser/preferences/preferences.js + content/browser/preferences/extensionControlled.js +* content/browser/preferences/preferences.xhtml + + content/browser/preferences/main.js + content/browser/preferences/home.js + content/browser/preferences/search.js + content/browser/preferences/privacy.js + content/browser/preferences/containers.js + content/browser/preferences/sync.js + content/browser/preferences/experimental.js + content/browser/preferences/moreFromMozilla.js + content/browser/preferences/fxaPairDevice.xhtml + content/browser/preferences/fxaPairDevice.js + content/browser/preferences/findInPage.js + content/browser/preferences/more-from-mozilla-qr-code-simple.svg + content/browser/preferences/more-from-mozilla-qr-code-simple-cn.svg + content/browser/preferences/web-appearance-dark.svg + content/browser/preferences/web-appearance-light.svg diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml new file mode 100644 index 0000000000..8f87b2f5d9 --- /dev/null +++ b/browser/components/preferences/main.inc.xhtml @@ -0,0 +1,837 @@ +# 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/. + +<!-- General panel --> + +<script src="chrome://browser/content/preferences/main.js"/> + +#ifdef MOZ_UPDATER + <script src="chrome://browser/content/aboutDialog-appUpdater.js"/> +#endif + +<script src="chrome://mozapps/content/preferences/fontbuilder.js"/> + +<html:template id="template-paneGeneral"> +<hbox id="generalCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="pane-general-title"/> +</hbox> + +<!-- Startup --> +<groupbox id="startupGroup" + data-category="paneGeneral" + hidden="true"> + <label><html:h2 data-l10n-id="startup-header"/></label> + + <vbox id="startupPageBox"> + <checkbox id="browserRestoreSession" + data-l10n-id="startup-restore-windows-and-tabs"/> + </vbox> + +#ifdef HAVE_SHELL_SERVICE + <vbox id="defaultBrowserBox"> + <checkbox id="alwaysCheckDefault" preference="browser.shell.checkDefaultBrowser" + disabled="true" + data-l10n-id="always-check-default"/> + <stack id="setDefaultPane"> + <hbox id="isNotDefaultPane" align="center" class="indent"> + <label class="face-sad" id="isNotDefaultLabel" flex="1" data-l10n-id="is-not-default"/> + <button id="setDefaultButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="set-as-my-default-browser" + preference="pref.general.disable_button.default_browser"/> + </hbox> + <hbox id="isDefaultPane" align="center" class="indent"> + <label class="face-smile" id="isDefaultLabel" flex="1" data-l10n-id="is-default"/> + </hbox> + </stack> + </vbox> +#endif + +</groupbox> + +<!-- Data migration --> +<groupbox id="dataMigrationGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="preferences-data-migration-header"/></label> + + <hbox id="dataMigration" flex="1"> + <description flex="1" control="data-migration" data-l10n-id="preferences-data-migration-description"/> + <button id="data-migration" + is="highlightable-button" + class="accessory-button" + data-l10n-id="preferences-data-migration-button"/> + </hbox> +</groupbox> + +<!-- Tab preferences --> +<groupbox data-category="paneGeneral" + hidden="true"> + <label><html:h2 data-l10n-id="tabs-group-header"/></label> + + <checkbox id="ctrlTabRecentlyUsedOrder" data-l10n-id="ctrl-tab-recently-used-order" + preference="browser.ctrlTab.sortByRecentlyUsed"/> + + <checkbox id="linkTargeting" data-l10n-id="open-new-link-as-tabs" + preference="browser.link.open_newwindow"/> + + <checkbox id="warnOpenMany" data-l10n-id="warn-on-open-many-tabs" + preference="browser.tabs.warnOnOpen"/> + + <checkbox id="switchToNewTabs" data-l10n-id="switch-to-new-tabs" + preference="browser.tabs.loadInBackground"/> + + <checkbox id="warnCloseMultiple" data-l10n-id="confirm-on-close-multiple-tabs" + preference="browser.tabs.warnOnClose"/> + +#ifndef XP_WIN + <checkbox id="warnOnQuitKey" preference="browser.warnOnQuitShortcut"/> +#endif + +#ifdef XP_WIN + <checkbox id="showTabsInTaskbar" data-l10n-id="show-tabs-in-taskbar" + preference="browser.taskbar.previews.enable"/> +#endif + + <vbox id="browserContainersbox" hidden="true"> + <hbox id="browserContainersExtensionContent" + align="center" class="extension-controlled info-box-container"> + <hbox flex="1"> + <description control="disableContainersExtension" class="description-with-side-element" flex="1" /> + </hbox> + <button id="disableContainersExtension" + is="highlightable-button" + class="extension-controlled-button accessory-button" + data-l10n-id="disable-extension" /> + </hbox> + <hbox align="center"> + <checkbox id="browserContainersCheckbox" + class="tail-with-learn-more" + data-l10n-id="browser-containers-enabled" + preference="privacy.userContext.enabled"/> + <html:a + is="moz-support-link" + support-page="containers" + data-l10n-id="browser-containers-learn-more" + /> + <spacer flex="1"/> + <button id="browserContainersSettings" + is="highlightable-button" + class="accessory-button" + data-l10n-id="browser-containers-settings" + search-l10n-ids="containers-add-button.label, + containers-settings-button.label, + containers-remove-button.label" + /> + </hbox> + </vbox> +</groupbox> + +<hbox id="languageAndAppearanceCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="language-and-appearance-header"/> +</hbox> + +<!-- Website appearance --> +<groupbox id="webAppearanceGroup" data-category="paneGeneral" hidden="true"> + <html:h2 data-l10n-id="preferences-web-appearance-header"/> + <html:div id="webAppearanceSettings"> + <description data-l10n-id="preferences-web-appearance-description"/> + <html:div id="web-appearance-override-warning" class="info-box-container"> + <html:div class="info-icon-container"> + <html:img class="info-icon"/> + </html:div> + <description data-l10n-id="preferences-web-appearance-override-warning"> + <html:a class="text-link" data-l10n-name="colors-link" id="web-appearance-manage-colors-link" href="#"/> + </description> + </html:div> + <form xmlns="http://www.w3.org/1999/xhtml" id="web-appearance-chooser" autocomplete="off"> + <label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-auto"> + <div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div> + <div class="web-appearance-choice-footer"> + <input type="radio" name="web-appearance" value="auto" data-l10n-id="preferences-web-appearance-choice-input-auto" + /><span data-l10n-id="preferences-web-appearance-choice-auto" /> + </div> + </label> + <label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-light"> + <div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div> + <div class="web-appearance-choice-footer"> + <input type="radio" name="web-appearance" value="light" data-l10n-id="preferences-web-appearance-choice-input-light" + /><span data-l10n-id="preferences-web-appearance-choice-light" /> + </div> + </label> + <label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-dark"> + <div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div> + <div class="web-appearance-choice-footer"> + <input type="radio" name="web-appearance" value="dark" data-l10n-id="preferences-web-appearance-choice-input-dark" + /><span data-l10n-id="preferences-web-appearance-choice-dark" /> + </div> + </label> + </form> + <html:div data-l10n-id="preferences-web-appearance-footer"> + <html:a id="web-appearance-manage-themes-link" class="text-link" data-l10n-name="themes-link" href="about:addons" target="_blank" /> + </html:div> + </html:div> +</groupbox> + +<!-- Colors --> +<groupbox id="colorsGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="preferences-colors-header"/></label> + + <hbox id="colorsSettings" align="center" flex="1"> + <description flex="1" control="colors" data-l10n-id="preferences-colors-description"/> + <button id="colors" + is="highlightable-button" + class="accessory-button" + data-l10n-id="preferences-colors-manage-button" + search-l10n-ids=" + colors-page-override, + colors-page-override-option-always.label, + colors-page-override-option-auto.label, + colors-page-override-option-never.label, + colors-text-and-background, + colors-text-header, + colors-background, + colors-use-system, + colors-underline-links, + colors-links-header, + colors-unvisited-links, + colors-visited-links + "/> + </hbox> +</groupbox> + +<!-- Fonts --> +<groupbox id="fontsGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="preferences-fonts-header"/></label> + + <hbox id="fontSettings"> + <hbox align="center" flex="1"> + <label control="defaultFont" data-l10n-id="default-font"/> + <menulist id="defaultFont" delayprefsave="true"/> + <label id="defaultFontSizeLabel" control="defaultFontSize" data-l10n-id="default-font-size"></label> + <menulist id="defaultFontSize" 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> + </hbox> + + <button id="advancedFonts" + is="highlightable-button" + class="accessory-button" + data-l10n-id="advanced-fonts" + search-l10n-ids=" + fonts-window.title, + fonts-langgroup-header, + fonts-proportional-size, + fonts-proportional-header, + fonts-serif, + fonts-sans-serif, + fonts-monospace, + fonts-langgroup-arabic.label, + fonts-langgroup-armenian.label, + fonts-langgroup-bengali.label, + fonts-langgroup-simpl-chinese.label, + fonts-langgroup-trad-chinese-hk.label, + fonts-langgroup-trad-chinese.label, + fonts-langgroup-cyrillic.label, + fonts-langgroup-devanagari.label, + fonts-langgroup-ethiopic.label, + fonts-langgroup-georgian.label, + fonts-langgroup-el.label, + fonts-langgroup-gujarati.label, + fonts-langgroup-gurmukhi.label, + fonts-langgroup-japanese.label, + fonts-langgroup-hebrew.label, + fonts-langgroup-kannada.label, + fonts-langgroup-khmer.label, + fonts-langgroup-korean.label, + fonts-langgroup-latin.label, + fonts-langgroup-malayalam.label, + fonts-langgroup-math.label, + fonts-langgroup-odia.label, + fonts-langgroup-sinhala.label, + fonts-langgroup-tamil.label, + fonts-langgroup-telugu.label, + fonts-langgroup-thai.label, + fonts-langgroup-tibetan.label, + fonts-langgroup-canadian.label, + fonts-langgroup-other.label, + fonts-minsize, + fonts-minsize-none.label, + fonts-default-serif.label, + fonts-default-sans-serif.label, + fonts-allow-own.label, + " /> + </hbox> +</groupbox> + +<!-- Zoom --> +<groupbox id="zoomGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="preferences-zoom-header"/></label> + + <hbox id="zoomBox" align="center" hidden="true"> + <label control="defaultZoom" data-l10n-id="preferences-default-zoom"/> + <menulist id="defaultZoom"> + <menupopup/> + </menulist> + </hbox> + + <checkbox id="zoomText" + data-l10n-id="preferences-zoom-text-only"/> + +</groupbox> + +<!-- Languages --> +<groupbox id="languagesGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="language-header"/></label> + + <vbox id="browserLanguagesBox" align="start" hidden="true"> + <description flex="1" controls="chooseBrowserLanguage" data-l10n-id="choose-browser-language-description"/> + <hbox> + <menulist id="primaryBrowserLocale"> + <menupopup/> + </menulist> + <button id="manageBrowserLanguagesButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="manage-browser-languages-button"/> + </hbox> + </vbox> + <hbox id="confirmBrowserLanguage" class="message-bar" align="center" hidden="true"> + <html:img class="message-bar-icon"/> + <vbox class="message-bar-content-container" align="stretch" flex="1"/> + </hbox> + + <hbox id="languagesBox" align="center"> + <description flex="1" control="chooseLanguage" data-l10n-id="choose-language-description"/> + <button id="chooseLanguage" + is="highlightable-button" + class="accessory-button" + data-l10n-id="choose-button" + search-l10n-ids=" + webpage-languages-window2.title, + languages-description, + languages-customize-moveup.label, + languages-customize-movedown.label, + languages-customize-remove.label, + languages-customize-select-language.placeholder, + languages-customize-add.label, + " /> + </hbox> + + <checkbox id="useSystemLocale" hidden="true" + data-l10n-id="use-system-locale" + data-l10n-args='{"localeName": "und"}' + preference="intl.regional_prefs.use_os_locales"/> + + <!-- TODO (Bug 1817084) This older implementation will be removed soon --> + <hbox id="translationBox" hidden="true"> + <hbox align="center" flex="1"> + <checkbox id="translate" preference="browser.translation.detectLanguage" + data-l10n-id="translate-web-pages"/> + <hbox id="bingAttribution" hidden="true" align="center"> + <label data-l10n-id="translate-attribution"> + <html:img id="translationAttributionImage" aria-label="Microsoft Translator" + src="chrome://browser/content/microsoft-translator-attribution.png" + data-l10n-name="logo"/> + </label> + </hbox> + </hbox> + <button id="translateButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="translate-exceptions"/> + </hbox> + + <hbox id="fxtranslationsBox" hidden="true" data-subcategory="fxtranslations"> + <description flex="1" control="fxtranslateButton" data-l10n-id="fx-translate-web-pages"/> + <button id="fxtranslateButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="translate-exceptions"/> + </hbox> + + <checkbox id="checkSpelling" + data-l10n-id="check-user-spelling" + preference="layout.spellcheckDefault"/> + + <!-- Translations --> + <vbox id="translationsGroup" hidden="true" data-subcategory="translations"> + <label><html:h2 data-l10n-id="translations-manage-header"/></label> + <hbox id="translations-manage-description" align="center"> + <description flex="1" data-l10n-id="translations-manage-description"/> + <button id="translations-manage-settings-button" + is="highlightable-button" + class="accessory-button" + data-l10n-id="translations-manage-settings-button"/> + </hbox> + <vbox> + <html:div id="translations-manage-install-list" hidden="true"> + <hbox class="translations-manage-language"> + <label data-l10n-id="translations-manage-all-language"></label> + <button id="translations-manage-install-all" + data-l10n-id="translations-manage-download-button"></button> + <button id="translations-manage-delete-all" + data-l10n-id="translations-manage-delete-button"></button> + </hbox> + <!-- The downloadable languages will be listed here. --> + </html:div> + <description id="translations-manage-error" hidden="true"></description> + </vbox> + </vbox> +</groupbox> + +<!-- Files and Applications --> +<hbox id="filesAndApplicationsCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="files-and-applications-title"/> +</hbox> + +<!--Downloads--> +<groupbox id="downloadsGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="download-header"/></label> + + <hbox id="saveWhere"> + <label id="saveTo" + control="downloadFolder" + data-l10n-id="download-save-where"/> + <html:input id="downloadFolder" + type="text" + readonly="readonly" + aria-labelledby="saveTo"/> + <button id="chooseFolder" + is="highlightable-button" + class="accessory-button" + data-l10n-id="download-choose-folder"/> + </hbox> + <checkbox id="alwaysAsk" + data-l10n-id="download-always-ask-where" + preference="browser.download.useDownloadDir"/> +</groupbox> + +<groupbox id="applicationsGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="applications-header"/></label> + <description data-l10n-id="applications-description"/> + <search-textbox id="filter" flex="1" + data-l10n-id="applications-filter" + data-l10n-attrs="placeholder" + aria-controls="handlersView"/> + + <listheader id="handlersViewHeader"> + <treecol id="typeColumn" data-l10n-id="applications-type-column" value="type" + persist="sortDirection" + style="flex: 1 50%" sortDirection="ascending"/> + <treecol id="actionColumn" data-l10n-id="applications-action-column" value="action" + persist="sortDirection" + style="flex: 1 50%"/> + </listheader> + <richlistbox id="handlersView" + preference="pref.downloads.disable_button.edit_actions"/> + <description id="handleNewFileTypesDesc" + data-l10n-id="applications-handle-new-file-types-description"/> + <radiogroup id="handleNewFileTypes" + preference="browser.download.always_ask_before_handling_new_types"> + <radio id="saveForNewTypes" + value="false" + data-l10n-id="applications-save-for-new-types"/> + <radio id="askBeforeHandling" + value="true" + data-l10n-id="applications-ask-before-handling"/> + </radiogroup> +</groupbox> + + +<!-- DRM Content --> +<groupbox id="drmGroup" data-category="paneGeneral" data-subcategory="drm" hidden="true"> + <label><html:h2 data-l10n-id="drm-content-header"/></label> + <hbox align="center"> + <checkbox id="playDRMContent" preference="media.eme.enabled" + class="tail-with-learn-more" data-l10n-id="play-drm-content" /> + <html:a is="moz-support-link" + data-l10n-id="play-drm-content-learn-more" + support-page="drm-content" + /> + </hbox> +</groupbox> + +<hbox id="updatesCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="update-application-title"/> +</hbox> + +<!-- Update --> +<groupbox id="updateApp" data-category="paneGeneral" hidden="true"> + <label class="search-header" hidden="true"><html:h2 data-l10n-id="update-application-title"/></label> + + <label data-l10n-id="update-application-description"/> + <hbox align="center"> + <vbox flex="1"> + <description id="updateAppInfo"> + <html:a id="releasenotes" target="_blank" data-l10n-name="learn-more" class="learnMore text-link" hidden="true"/> + </description> + <description id="distribution" class="text-blurb" hidden="true"/> + <description id="distributionId" class="text-blurb" hidden="true"/> + </vbox> +#ifdef MOZ_UPDATER + <button id="showUpdateHistory" + is="highlightable-button" + class="accessory-button" + data-l10n-id="update-history" + preference="app.update.disable_button.showUpdateHistory" + search-l10n-ids=" + history-title, + history-intro + "/> +#endif + </hbox> +#ifdef MOZ_UPDATER + <vbox id="updateBox"> + <deck id="updateDeck" orient="vertical"> + <hbox id="checkForUpdates" align="start"> + <spacer flex="1"/> + <button id="checkForUpdatesButton" + is="highlightable-button" + data-l10n-id="update-checkForUpdatesButton"/> + </hbox> + <hbox id="downloadAndInstall" align="start"> + <spacer flex="1"/> + <button id="downloadAndInstallButton" + is="highlightable-button"/> + <!-- label and accesskey will be filled by JS --> + </hbox> + <hbox id="apply" align="start"> + <spacer flex="1"/> + <button id="updateButton" + is="highlightable-button" + data-l10n-id="update-updateButton"/> + </hbox> + <hbox id="checkingForUpdates" align="start"> + <html:img class="update-throbber"/> + <label data-l10n-id="update-checkingForUpdates"/> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="downloading" align="start" data-l10n-id="settings-update-downloading" data-l10n-args='{"transfer":""}'> + <html:img class="update-throbber" data-l10n-name="icon"/> + <label data-l10n-name="download-status"/> + </hbox> + <hbox id="applying" align="start"> + <html:img class="update-throbber"/> + <label data-l10n-id="update-applying"/> + </hbox> + <hbox id="downloadFailed" align="start"> + <label data-l10n-id="update-failed-main"> + <html:a id="failedLink" target="_blank" class="learnMore text-link" data-l10n-name="failed-link-main"></html:a> + </label> + <button id="checkForUpdatesButton2" + data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button"/> + </hbox> + <hbox id="policyDisabled" align="start"> + <label data-l10n-id="update-adminDisabled"/> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="noUpdatesFound" align="start"> + <label class="face-smile" data-l10n-id="update-noUpdatesFound"/> + <button id="checkForUpdatesButton3" + data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button"/> + </hbox> + <hbox id="checkingFailed" align="start"> + <label class="face-sad" data-l10n-id="aboutdialog-update-checking-failed"/> + <button id="checkForUpdatesButton4" + data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button"/> + </hbox> + <hbox id="otherInstanceHandlingUpdates" align="start"> + <label data-l10n-id="update-otherInstanceHandlingUpdates"/> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="manualUpdate" align="start"> + <description class="face-sad" data-l10n-id="settings-update-manual-with-link" data-l10n-args='{"displayUrl":""}'> + <html:a class="manualLink" data-l10n-name="manual-link" target="_blank"/> + </description> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="unsupportedSystem" align="start"> + <description flex="1" data-l10n-id="update-unsupported"> + <label id="unsupportedLink" class="learnMore" data-l10n-name="unsupported-link" is="text-link"></label> + </description> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="restarting" align="start"> + <html:img class="update-throbber"/> + <label data-l10n-id="update-restarting"/> + <button data-l10n-id="update-updateButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="internalError" align="start"> + <description class="face-sad" flex="1" data-l10n-id="update-internal-error2" data-l10n-args='{"displayUrl":""}'> + <label class="manualLink" data-l10n-name="manual-link" is="text-link"/> + </description> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + </deck> + </vbox> +#endif + +#ifdef MOZ_UPDATER + <description id="updateAllowDescription" data-l10n-id="update-application-allow-description"></description> + <vbox id="updateSettingsContainer" class="info-box-container"> + <radiogroup id="updateRadioGroup"> + <radio id="autoDesktop" + value="true" + data-l10n-id="update-application-auto"/> +#ifdef MOZ_UPDATE_AGENT + <checkbox id="backgroundUpdate" + class="indent" + hidden="true" + data-l10n-id="update-application-background-enabled"/> +#endif + <radio id="manualDesktop" + value="false" + data-l10n-id="update-application-check-choose"/> + </radiogroup> + <hbox id="updateSettingCrossUserWarningDesc" hidden="true"> + <hbox class="info-icon-container"> + <html:img class="info-icon"/> + </hbox> + <description id="updateSettingCrossUserWarning" + flex="1" + data-l10n-id="update-application-warning-cross-user-setting"> + </description> + </hbox> + </vbox> +#ifdef MOZ_MAINTENANCE_SERVICE + <checkbox id="useService" + data-l10n-id="update-application-use-service" + preference="app.update.service.enabled"/> +#endif +#ifdef NIGHTLY_BUILD + <checkbox id="showUpdatePrompts" + data-l10n-id="update-application-suppress-prompts" + preference="app.update.suppressPrompts"/> +#endif +#endif +</groupbox> + +<hbox id="performanceCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="performance-title"/> +</hbox> + +<!-- Performance --> +<groupbox id="performanceGroup" data-category="paneGeneral" hidden="true"> + <label class="search-header" hidden="true"><html:h2 data-l10n-id="performance-title"/></label> + + <hbox align="center"> + <checkbox id="useRecommendedPerformanceSettings" + class="tail-with-learn-more" + data-l10n-id="performance-use-recommended-settings-checkbox" + preference="browser.preferences.defaultPerformanceSettings.enabled"/> + <html:a is="moz-support-link" + data-l10n-id="performance-settings-learn-more" + support-page="performance" + /> + </hbox> + <description class="indent tip-caption" data-l10n-id="performance-use-recommended-settings-desc"/> + + <vbox id="performanceSettings" class="indent" hidden="true"> + <checkbox id="allowHWAccel" + data-l10n-id="performance-allow-hw-accel" + preference="layers.acceleration.disabled"/> + <hbox align="center"> + <label id="limitContentProcess" data-l10n-id="performance-limit-content-process-option" control="contentProcessCount"/> + <menulist id="contentProcessCount" preference="dom.ipc.processCount"> + <menupopup> + <menuitem label="1" value="1"/> + <menuitem label="2" value="2"/> + <menuitem label="3" value="3"/> + <menuitem label="4" value="4"/> + <menuitem label="5" value="5"/> + <menuitem label="6" value="6"/> + <menuitem label="7" value="7"/> + <menuitem label="8" value="8"/> + </menupopup> + </menulist> + </hbox> + <description id="contentProcessCountEnabledDescription" class="tip-caption" data-l10n-id="performance-limit-content-process-enabled-desc"/> + <description id="contentProcessCountDisabledDescription" class="tip-caption" data-l10n-id="performance-limit-content-process-blocked-desc"> + <html:a class="text-link" data-l10n-name="learn-more" href="https://wiki.mozilla.org/Electrolysis"/> + </description> + </vbox> +</groupbox> + +<hbox id="browsingCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="browsing-title"/> +</hbox> + +<!-- Browsing --> +<groupbox id="browsingGroup" data-category="paneGeneral" hidden="true"> + <label class="search-header" hidden="true"><html:h2 data-l10n-id="browsing-title"/></label> + + <checkbox id="useAutoScroll" + data-l10n-id="browsing-use-autoscroll" + preference="general.autoScroll"/> + <checkbox id="useSmoothScrolling" + data-l10n-id="browsing-use-smooth-scrolling" + preference="general.smoothScroll"/> +#ifdef MOZ_WIDGET_GTK + <checkbox id="useOverlayScrollbars" + data-l10n-id="browsing-gtk-use-non-overlay-scrollbars" + preference="widget.gtk.overlay-scrollbars.enabled"/> +#endif +#ifdef XP_WIN + <checkbox id="useOnScreenKeyboard" + hidden="true" + data-l10n-id="browsing-use-onscreen-keyboard" + preference="ui.osk.enabled"/> +#endif + <checkbox id="useCursorNavigation" + data-l10n-id="browsing-use-cursor-navigation" + preference="accessibility.browsewithcaret"/> + <checkbox id="searchStartTyping" + data-l10n-id="browsing-search-on-start-typing" + preference="accessibility.typeaheadfind"/> + <hbox id="pictureInPictureBox" align="center" hidden="true"> + <checkbox id="pictureInPictureToggleEnabled" + class="tail-with-learn-more" + data-l10n-id="browsing-picture-in-picture-toggle-enabled" + preference="media.videocontrols.picture-in-picture.video-toggle.enabled"/> + <html:a is="moz-support-link" + data-l10n-id="browsing-picture-in-picture-learn-more" + support-page="picture-in-picture" + /> + </hbox> + <hbox id="mediaControlBox" align="center" hidden="true"> + <checkbox id="mediaControlToggleEnabled" + class="tail-with-learn-more" + data-l10n-id="browsing-media-control" + preference="media.hardwaremediakeys.enabled"/> + <html:a is="moz-support-link" + data-l10n-id="browsing-media-control-learn-more" + support-page="media-keyboard-control" + /> + </hbox> + <hbox align="center" data-subcategory="cfraddons"> + <checkbox id="cfrRecommendations" + class="tail-with-learn-more" + data-l10n-id="browsing-cfr-recommendations" + preference="browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons"/> + <html:a is="moz-support-link" + data-l10n-id="browsing-cfr-recommendations-learn-more" + support-page="extensionrecommendations" + /> + </hbox> + <hbox align="center" data-subcategory="cfrfeatures"> + <checkbox id="cfrRecommendations-features" + class="tail-with-learn-more" + data-l10n-id="browsing-cfr-features" + preference="browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features"/> + <html:a is="moz-support-link" + data-l10n-id="browsing-cfr-recommendations-learn-more" + support-page="extensionrecommendations" + /> + </hbox> +</groupbox> + +<hbox id="networkProxyCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="network-settings-title"/> +</hbox> + +<!-- Network Settings--> +<groupbox id="connectionGroup" data-category="paneGeneral" hidden="true"> + <label class="search-header" hidden="true"><html:h2 data-l10n-id="network-settings-title"/></label> + + <hbox align="center" + data-subcategory="netsettings"> + <description flex="1" control="connectionSettings"> + <html:span id="connectionSettingsDescription"/> + <html:a is="moz-support-link" + data-l10n-id="network-proxy-connection-learn-more" + support-page="prefs-connection-settings" + /> + </description> + <separator orient="vertical"/> + <button id="connectionSettings" + is="highlightable-button" + class="accessory-button" + data-l10n-id="network-proxy-connection-settings" + search-l10n-ids=" + connection-window2.title, + connection-proxy-option-no.label, + connection-proxy-option-auto.label, + connection-proxy-option-system.label, + connection-proxy-option-manual.label, + connection-proxy-http, + connection-proxy-https, + connection-proxy-http-port, + connection-proxy-socks, + connection-proxy-socks4, + connection-proxy-socks5, + connection-proxy-noproxy, + connection-proxy-noproxy-desc, + connection-proxy-https-sharing.label, + connection-proxy-autotype.label, + connection-proxy-reload.label, + connection-proxy-autologin.label, + connection-proxy-socks-remote-dns.label, + " /> + </hbox> +</groupbox> +</html:template> diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js new file mode 100644 index 0000000000..3f5db88709 --- /dev/null +++ b/browser/components/preferences/main.js @@ -0,0 +1,4258 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from extensionControlled.js */ +/* import-globals-from preferences.js */ +/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */ +/* import-globals-from /browser/base/content/aboutDialog-appUpdater.js */ +/* global MozXULElement */ + +ChromeUtils.defineESModuleGetters(this, { + BackgroundUpdate: "resource://gre/modules/BackgroundUpdate.sys.mjs", + MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", +}); + +// Constants & Enumeration Values +const TYPE_PDF = "application/pdf"; + +const PREF_PDFJS_DISABLED = "pdfjs.disabled"; + +// Pref for when containers is being controlled +const PREF_CONTAINERS_EXTENSION = "privacy.userContext.extension"; + +// Strings to identify ExtensionSettingsStore overrides +const CONTAINERS_KEY = "privacy.containers"; + +const PREF_USE_SYSTEM_COLORS = "browser.display.use_system_colors"; +const PREF_CONTENT_APPEARANCE = + "layout.css.prefers-color-scheme.content-override"; +const FORCED_COLORS_QUERY = matchMedia("(forced-colors)"); + +const AUTO_UPDATE_CHANGED_TOPIC = + UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic; +const BACKGROUND_UPDATE_CHANGED_TOPIC = + UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"] + .observerTopic; + +const ICON_URL_APP = + AppConstants.platform == "linux" + ? "moz-icon://dummy.exe?size=16" + : "chrome://browser/skin/preferences/application.png"; + +// For CSS. Can be one of "ask", "save" or "handleInternally". If absent, the icon URL +// was set by us to a custom handler icon and CSS should not try to override it. +const APP_ICON_ATTR_NAME = "appHandlerIcon"; + +Preferences.addAll([ + // Startup + { id: "browser.startup.page", type: "int" }, + { id: "browser.privatebrowsing.autostart", type: "bool" }, + + // Downloads + { id: "browser.download.useDownloadDir", type: "bool", inverted: true }, + { id: "browser.download.always_ask_before_handling_new_types", type: "bool" }, + { id: "browser.download.folderList", type: "int" }, + { id: "browser.download.dir", type: "file" }, + + /* Tab preferences + Preferences: + + browser.link.open_newwindow + 1 opens such links in the most recent window or tab, + 2 opens such links in a new window, + 3 opens such links in a new tab + browser.tabs.loadInBackground + - true if display should switch to a new tab which has been opened from a + link, false if display shouldn't switch + browser.tabs.warnOnClose + - true if when closing a window with multiple tabs the user is warned and + allowed to cancel the action, false to just close the window + browser.tabs.warnOnOpen + - true if the user should be warned if he attempts to open a lot of tabs at + once (e.g. a large folder of bookmarks), false otherwise + browser.warnOnQuitShortcut + - true if the user should be warned if they quit using the keyboard shortcut + browser.taskbar.previews.enable + - true if tabs are to be shown in the Windows 7 taskbar + */ + + { id: "browser.link.open_newwindow", type: "int" }, + { id: "browser.tabs.loadInBackground", type: "bool", inverted: true }, + { id: "browser.tabs.warnOnClose", type: "bool" }, + { id: "browser.warnOnQuitShortcut", type: "bool" }, + { id: "browser.tabs.warnOnOpen", type: "bool" }, + { id: "browser.ctrlTab.sortByRecentlyUsed", type: "bool" }, + + // CFR + { + id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", + type: "bool", + }, + { + id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + type: "bool", + }, + + // Fonts + { id: "font.language.group", type: "wstring" }, + + // Languages + { id: "browser.translation.detectLanguage", type: "bool" }, + { id: "intl.regional_prefs.use_os_locales", type: "bool" }, + + // General tab + + /* Accessibility + * accessibility.browsewithcaret + - true enables keyboard navigation and selection within web pages using a + visible caret, false uses normal keyboard navigation with no caret + * accessibility.typeaheadfind + - when set to true, typing outside text areas and input boxes will + automatically start searching for what's typed within the current + document; when set to false, no search action happens */ + { id: "accessibility.browsewithcaret", type: "bool" }, + { id: "accessibility.typeaheadfind", type: "bool" }, + { id: "accessibility.blockautorefresh", type: "bool" }, + + /* Browsing + * general.autoScroll + - when set to true, clicking the scroll wheel on the mouse activates a + mouse mode where moving the mouse down scrolls the document downward with + speed correlated with the distance of the cursor from the original + position at which the click occurred (and likewise with movement upward); + if false, this behavior is disabled + * general.smoothScroll + - set to true to enable finer page scrolling than line-by-line on page-up, + page-down, and other such page movements */ + { id: "general.autoScroll", type: "bool" }, + { id: "general.smoothScroll", type: "bool" }, + { id: "widget.gtk.overlay-scrollbars.enabled", type: "bool", inverted: true }, + { id: "layout.spellcheckDefault", type: "int" }, + + { + id: "browser.preferences.defaultPerformanceSettings.enabled", + type: "bool", + }, + { id: "dom.ipc.processCount", type: "int" }, + { id: "dom.ipc.processCount.web", type: "int" }, + { id: "layers.acceleration.disabled", type: "bool", inverted: true }, + + // Files and Applications + { id: "pref.downloads.disable_button.edit_actions", type: "bool" }, + + // DRM content + { id: "media.eme.enabled", type: "bool" }, + + // Update + { id: "browser.preferences.advanced.selectedTabIndex", type: "int" }, + { id: "browser.search.update", type: "bool" }, + + { id: "privacy.userContext.enabled", type: "bool" }, + { + id: "privacy.userContext.newTabContainerOnLeftClick.enabled", + type: "bool", + }, + + // Picture-in-Picture + { + id: "media.videocontrols.picture-in-picture.video-toggle.enabled", + type: "bool", + }, + + // Media + { id: "media.hardwaremediakeys.enabled", type: "bool" }, +]); + +if (AppConstants.HAVE_SHELL_SERVICE) { + Preferences.addAll([ + { id: "browser.shell.checkDefaultBrowser", type: "bool" }, + { id: "pref.general.disable_button.default_browser", type: "bool" }, + ]); +} + +if (AppConstants.platform === "win") { + Preferences.addAll([ + { id: "browser.taskbar.previews.enable", type: "bool" }, + { id: "ui.osk.enabled", type: "bool" }, + ]); +} + +if (AppConstants.MOZ_UPDATER) { + Preferences.addAll([ + { id: "app.update.disable_button.showUpdateHistory", type: "bool" }, + ]); + + if (AppConstants.NIGHTLY_BUILD) { + Preferences.addAll([{ id: "app.update.suppressPrompts", type: "bool" }]); + } + + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + Preferences.addAll([{ id: "app.update.service.enabled", type: "bool" }]); + } +} + +XPCOMUtils.defineLazyGetter(this, "gIsPackagedApp", () => { + return Services.sysinfo.getProperty("isPackagedApp"); +}); + +// A promise that resolves when the list of application handlers is loaded. +// We store this in a global so tests can await it. +var promiseLoadHandlersList; + +// Load the preferences string bundle for other locales with fallbacks. +function getBundleForLocales(newLocales) { + let locales = Array.from( + new Set([ + ...newLocales, + ...Services.locale.requestedLocales, + Services.locale.lastFallbackLocale, + ]) + ); + return new Localization( + ["browser/preferences/preferences.ftl", "branding/brand.ftl"], + false, + undefined, + locales + ); +} + +var gNodeToObjectMap = new WeakMap(); + +var gMainPane = { + // The set of types the app knows how to handle. A hash of HandlerInfoWrapper + // objects, indexed by type. + _handledTypes: {}, + + // The list of types we can show, sorted by the sort column/direction. + // An array of HandlerInfoWrapper objects. We build this list when we first + // load the data and then rebuild it when users change a pref that affects + // what types we can show or change the sort column/direction. + // Note: this isn't necessarily the list of types we *will* show; if the user + // provides a filter string, we'll only show the subset of types in this list + // that match that string. + _visibleTypes: [], + + // browser.startup.page values + STARTUP_PREF_BLANK: 0, + STARTUP_PREF_HOMEPAGE: 1, + STARTUP_PREF_RESTORE_SESSION: 3, + + // Convenience & Performance Shortcuts + + get _list() { + delete this._list; + return (this._list = document.getElementById("handlersView")); + }, + + get _filter() { + delete this._filter; + return (this._filter = document.getElementById("filter")); + }, + + _backoffIndex: 0, + + /** + * Initialization of gMainPane. + */ + init() { + function setEventListener(aId, aEventType, aCallback) { + document + .getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gMainPane)); + } + + if (AppConstants.HAVE_SHELL_SERVICE) { + this.updateSetDefaultBrowser(); + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // Exponential backoff mechanism will delay the polling times if user doesn't + // trigger SetDefaultBrowser for a long time. + let backoffTimes = [ + 1000, 1000, 1000, 1000, 2000, 2000, 2000, 5000, 5000, 10000, + ]; + + let pollForDefaultBrowser = () => { + let uri = win.gBrowser.currentURI.spec; + + if ( + (uri == "about:preferences" || uri == "about:preferences#general") && + document.visibilityState == "visible" + ) { + this.updateSetDefaultBrowser(); + } + + // approximately a "requestIdleInterval" + window.setTimeout(() => { + window.requestIdleCallback(pollForDefaultBrowser); + }, backoffTimes[this._backoffIndex + 1 < backoffTimes.length ? this._backoffIndex++ : backoffTimes.length - 1]); + }; + + window.setTimeout(() => { + window.requestIdleCallback(pollForDefaultBrowser); + }, backoffTimes[this._backoffIndex]); + } + + this.initBrowserContainers(); + this.buildContentProcessCountMenuList(); + + this.updateDefaultPerformanceSettingsPref(); + + let defaultPerformancePref = Preferences.get( + "browser.preferences.defaultPerformanceSettings.enabled" + ); + defaultPerformancePref.on("change", () => { + this.updatePerformanceSettingsBox({ duringChangeEvent: true }); + }); + this.updatePerformanceSettingsBox({ duringChangeEvent: false }); + this.displayUseSystemLocale(); + this.updateProxySettingsUI(); + initializeProxyUI(gMainPane); + + if (Services.prefs.getBoolPref("intl.multilingual.enabled")) { + gMainPane.initPrimaryBrowserLanguageUI(); + } + + // We call `initDefaultZoomValues` to set and unhide the + // default zoom preferences menu, and to establish a + // listener for future menu changes. + gMainPane.initDefaultZoomValues(); + + gMainPane.initTranslations(); + + if ( + Services.prefs.getBoolPref( + "media.videocontrols.picture-in-picture.enabled" + ) + ) { + document.getElementById("pictureInPictureBox").hidden = false; + setEventListener( + "pictureInPictureToggleEnabled", + "command", + function (event) { + if (!event.target.checked) { + Services.telemetry.recordEvent( + "pictureinpicture.settings", + "disable", + "settings" + ); + } + } + ); + } + + if (AppConstants.platform == "win") { + // Functionality for "Show tabs in taskbar" on Windows 7 and up. + try { + let ver = parseFloat(Services.sysinfo.getProperty("version")); + let showTabsInTaskbar = document.getElementById("showTabsInTaskbar"); + showTabsInTaskbar.hidden = ver < 6.1; + } catch (ex) {} + } + + // The "opening multiple tabs might slow down Firefox" warning provides + // an option for not showing this warning again. When the user disables it, + // we provide checkboxes to re-enable the warning. + if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen")) { + document.getElementById("warnOpenMany").hidden = true; + } + + if (AppConstants.platform != "win") { + let quitKeyElement = + window.browsingContext.topChromeWindow.document.getElementById( + "key_quitApplication" + ); + if (quitKeyElement) { + let quitKey = ShortcutUtils.prettifyShortcut(quitKeyElement); + document.l10n.setAttributes( + document.getElementById("warnOnQuitKey"), + "confirm-on-quit-with-key", + { quitKey } + ); + } else { + // If the quit key element does not exist, then the quit key has + // been disabled, so just hide the checkbox. + document.getElementById("warnOnQuitKey").hidden = true; + } + } + + setEventListener("ctrlTabRecentlyUsedOrder", "command", function () { + Services.prefs.clearUserPref("browser.ctrlTab.migrated"); + }); + setEventListener("manageBrowserLanguagesButton", "command", function () { + gMainPane.showBrowserLanguagesSubDialog({ search: false }); + }); + if (AppConstants.MOZ_UPDATER) { + // These elements are only compiled in when the updater is enabled + setEventListener("checkForUpdatesButton", "command", function () { + gAppUpdater.checkForUpdates(); + }); + setEventListener("downloadAndInstallButton", "command", function () { + gAppUpdater.startDownload(); + }); + setEventListener("updateButton", "command", function () { + gAppUpdater.buttonRestartAfterDownload(); + }); + setEventListener("checkForUpdatesButton2", "command", function () { + gAppUpdater.checkForUpdates(); + }); + setEventListener("checkForUpdatesButton3", "command", function () { + gAppUpdater.checkForUpdates(); + }); + setEventListener("checkForUpdatesButton4", "command", function () { + gAppUpdater.checkForUpdates(); + }); + } + + // Startup pref + setEventListener( + "browserRestoreSession", + "command", + gMainPane.onBrowserRestoreSessionChange + ); + gMainPane.updateBrowserStartupUI = + gMainPane.updateBrowserStartupUI.bind(gMainPane); + Preferences.get("browser.privatebrowsing.autostart").on( + "change", + gMainPane.updateBrowserStartupUI + ); + Preferences.get("browser.startup.page").on( + "change", + gMainPane.updateBrowserStartupUI + ); + Preferences.get("browser.startup.homepage").on( + "change", + gMainPane.updateBrowserStartupUI + ); + gMainPane.updateBrowserStartupUI(); + + if (AppConstants.HAVE_SHELL_SERVICE) { + setEventListener( + "setDefaultButton", + "command", + gMainPane.setDefaultBrowser + ); + } + setEventListener( + "disableContainersExtension", + "command", + makeDisableControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY) + ); + setEventListener("chooseLanguage", "command", gMainPane.showLanguages); + setEventListener( + "translationAttributionImage", + "click", + gMainPane.openTranslationProviderAttribution + ); + // TODO (Bug 1817084) Remove this code when we disable the extension + setEventListener( + "translateButton", + "command", + gMainPane.showTranslationExceptions + ); + // TODO (Bug 1817084) Remove this code when we disable the extension + setEventListener( + "fxtranslateButton", + "command", + gMainPane.showTranslationExceptions + ); + Preferences.get("font.language.group").on( + "change", + gMainPane._rebuildFonts.bind(gMainPane) + ); + setEventListener("advancedFonts", "command", gMainPane.configureFonts); + setEventListener("colors", "command", gMainPane.configureColors); + Preferences.get("layers.acceleration.disabled").on( + "change", + gMainPane.updateHardwareAcceleration.bind(gMainPane) + ); + setEventListener( + "connectionSettings", + "command", + gMainPane.showConnections + ); + setEventListener( + "browserContainersCheckbox", + "command", + gMainPane.checkBrowserContainers + ); + setEventListener( + "browserContainersSettings", + "command", + gMainPane.showContainerSettings + ); + setEventListener( + "data-migration", + "command", + gMainPane.onMigrationButtonCommand + ); + + document + .getElementById("migrationWizardDialog") + .addEventListener("MigrationWizard:Close", function (e) { + e.currentTarget.close(); + }); + + if (Services.policies && !Services.policies.isAllowed("profileImport")) { + document.getElementById("dataMigrationGroup").remove(); + } + + // For media control toggle button, we support it on Windows 8.1+ (NT6.3), + // MacOs 10.4+ (darwin8.0, but we already don't support that) and + // gtk-based Linux. + if ( + AppConstants.isPlatformAndVersionAtLeast("win", "6.3") || + AppConstants.platform == "macosx" || + AppConstants.MOZ_WIDGET_GTK + ) { + document.getElementById("mediaControlBox").hidden = false; + } + + // Initializes the fonts dropdowns displayed in this pane. + this._rebuildFonts(); + + this.updateOnScreenKeyboardVisibility(); + + // Show translation preferences if we may: + const translationsPrefName = "browser.translation.ui.show"; + if (Services.prefs.getBoolPref(translationsPrefName)) { + let row = document.getElementById("translationBox"); + row.removeAttribute("hidden"); + // Showing attribution only for Bing Translator. + var { Translation } = ChromeUtils.import( + "resource:///modules/translation/TranslationParent.jsm" + ); + if (Translation.translationEngine == "Bing") { + document.getElementById("bingAttribution").removeAttribute("hidden"); + } + } + + // Firefox Translations settings panel + // TODO (Bug 1817084) Remove this code when we disable the extension + const fxtranslationsDisabledPrefName = "extensions.translations.disabled"; + if (!Services.prefs.getBoolPref(fxtranslationsDisabledPrefName, true)) { + let fxtranslationRow = document.getElementById("fxtranslationsBox"); + fxtranslationRow.hidden = false; + } + + let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled"); + // Force-disable/hide on WinXP: + if (navigator.platform.toLowerCase().startsWith("win")) { + emeUIEnabled = + emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6; + } + if (!emeUIEnabled) { + // Don't want to rely on .hidden for the toplevel groupbox because + // of the pane hiding/showing code potentially interfering: + document + .getElementById("drmGroup") + .setAttribute("style", "display: none !important"); + } + // Initialize the Firefox Updates section. + let version = AppConstants.MOZ_APP_VERSION_DISPLAY; + + // Include the build ID if this is an "a#" (nightly) build + if (/a\d+$/.test(version)) { + let buildID = Services.appinfo.appBuildID; + let year = buildID.slice(0, 4); + let month = buildID.slice(4, 6); + let day = buildID.slice(6, 8); + version += ` (${year}-${month}-${day})`; + } + + // Append "(32-bit)" or "(64-bit)" build architecture to the version number: + let bundle = Services.strings.createBundle( + "chrome://browser/locale/browser.properties" + ); + let archResource = Services.appinfo.is64Bit + ? "aboutDialog.architecture.sixtyFourBit" + : "aboutDialog.architecture.thirtyTwoBit"; + let arch = bundle.GetStringFromName(archResource); + version += ` (${arch})`; + + document.l10n.setAttributes( + document.getElementById("updateAppInfo"), + "update-application-version", + { version } + ); + + // Show a release notes link if we have a URL. + let relNotesLink = document.getElementById("releasenotes"); + let relNotesPrefType = Services.prefs.getPrefType("app.releaseNotesURL"); + if (relNotesPrefType != Services.prefs.PREF_INVALID) { + let relNotesURL = Services.urlFormatter.formatURLPref( + "app.releaseNotesURL" + ); + if (relNotesURL != "about:blank") { + relNotesLink.href = relNotesURL; + relNotesLink.hidden = false; + } + } + + let defaults = Services.prefs.getDefaultBranch(null); + let distroId = defaults.getCharPref("distribution.id", ""); + if (distroId) { + let distroString = distroId; + + let distroVersion = defaults.getCharPref("distribution.version", ""); + if (distroVersion) { + distroString += " - " + distroVersion; + } + + let distroIdField = document.getElementById("distributionId"); + distroIdField.value = distroString; + distroIdField.hidden = false; + + let distroAbout = defaults.getStringPref("distribution.about", ""); + if (distroAbout) { + let distroField = document.getElementById("distribution"); + distroField.value = distroAbout; + distroField.hidden = false; + } + } + + if (AppConstants.MOZ_UPDATER) { + gAppUpdater = new appUpdater(); + setEventListener("showUpdateHistory", "command", gMainPane.showUpdates); + + let updateDisabled = + Services.policies && !Services.policies.isAllowed("appUpdate"); + + if (gIsPackagedApp) { + // When we're running inside an app package, there's no point in + // displaying any update content here, and it would get confusing if we + // did, because our updater is not enabled. + // We can't rely on the hidden attribute for the toplevel elements, + // because of the pane hiding/showing code interfering. + document + .getElementById("updatesCategory") + .setAttribute("style", "display: none !important"); + document + .getElementById("updateApp") + .setAttribute("style", "display: none !important"); + } else if ( + updateDisabled || + UpdateUtils.appUpdateAutoSettingIsLocked() || + gApplicationUpdateService.manualUpdateOnly + ) { + document.getElementById("updateAllowDescription").hidden = true; + document.getElementById("updateSettingsContainer").hidden = true; + if (updateDisabled && AppConstants.MOZ_MAINTENANCE_SERVICE) { + document.getElementById("useService").hidden = true; + } + } else { + // Start with no option selected since we are still reading the value + document.getElementById("autoDesktop").removeAttribute("selected"); + document.getElementById("manualDesktop").removeAttribute("selected"); + // Start reading the correct value from the disk + this.readUpdateAutoPref(); + setEventListener("updateRadioGroup", "command", event => { + if (event.target.id == "backgroundUpdate") { + this.writeBackgroundUpdatePref(); + } else { + this.writeUpdateAutoPref(); + } + }); + if (this.isBackgroundUpdateUIAvailable()) { + document.getElementById("backgroundUpdate").hidden = false; + // Start reading the background update pref's value from the disk. + this.readBackgroundUpdatePref(); + } + } + + if (AppConstants.platform == "win") { + // On Windows, the Application Update setting is an installation- + // specific preference, not a profile-specific one. Show a warning to + // inform users of this. + let updateContainer = document.getElementById( + "updateSettingsContainer" + ); + updateContainer.classList.add("updateSettingCrossUserWarningContainer"); + document.getElementById( + "updateSettingCrossUserWarningDesc" + ).hidden = false; + } + + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + // Check to see if the maintenance service is installed. + // If it isn't installed, don't show the preference at all. + let installed; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + wrk.open( + wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64 + ); + installed = wrk.readIntValue("Installed"); + wrk.close(); + } catch (e) {} + if (installed != 1) { + document.getElementById("useService").hidden = true; + } + } + } + + // Initilize Application section. + + // Observe preferences that influence what we display so we can rebuild + // the view when they change. + Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC); + Services.obs.addObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC); + + setEventListener("filter", "command", gMainPane.filter); + setEventListener("typeColumn", "click", gMainPane.sort); + setEventListener("actionColumn", "click", gMainPane.sort); + setEventListener("chooseFolder", "command", gMainPane.chooseFolder); + Preferences.get("browser.download.folderList").on( + "change", + gMainPane.displayDownloadDirPref.bind(gMainPane) + ); + Preferences.get("browser.download.dir").on( + "change", + gMainPane.displayDownloadDirPref.bind(gMainPane) + ); + gMainPane.displayDownloadDirPref(); + + // Listen for window unload so we can remove our preference observers. + window.addEventListener("unload", this); + + // Figure out how we should be sorting the list. We persist sort settings + // across sessions, so we can't assume the default sort column/direction. + // XXX should we be using the XUL sort service instead? + if (document.getElementById("actionColumn").hasAttribute("sortDirection")) { + this._sortColumn = document.getElementById("actionColumn"); + // The typeColumn element always has a sortDirection attribute, + // either because it was persisted or because the default value + // from the xul file was used. If we are sorting on the other + // column, we should remove it. + document.getElementById("typeColumn").removeAttribute("sortDirection"); + } else { + this._sortColumn = document.getElementById("typeColumn"); + } + + let browserBundle = document.getElementById("browserBundle"); + appendSearchKeywords("browserContainersSettings", [ + browserBundle.getString("userContextPersonal.label"), + browserBundle.getString("userContextWork.label"), + browserBundle.getString("userContextBanking.label"), + browserBundle.getString("userContextShopping.label"), + ]); + + AppearanceChooser.init(); + + // Notify observers that the UI is now ready + Services.obs.notifyObservers(window, "main-pane-loaded"); + + Preferences.addSyncFromPrefListener( + document.getElementById("defaultFont"), + element => FontBuilder.readFontSelection(element) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("translate"), + () => + this.updateButtons( + "translateButton", + "browser.translation.detectLanguage" + ) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("checkSpelling"), + () => this.readCheckSpelling() + ); + Preferences.addSyncToPrefListener( + document.getElementById("checkSpelling"), + () => this.writeCheckSpelling() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("alwaysAsk"), + () => this.readUseDownloadDir() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("linkTargeting"), + () => this.readLinkTarget() + ); + Preferences.addSyncToPrefListener( + document.getElementById("linkTargeting"), + () => this.writeLinkTarget() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("browserContainersCheckbox"), + () => this.readBrowserContainersCheckbox() + ); + + this.setInitialized(); + }, + + preInit() { + promiseLoadHandlersList = new Promise((resolve, reject) => { + // Load the data and build the list of handlers for applications pane. + // By doing this after pageshow, we ensure it doesn't delay painting + // of the preferences page. + window.addEventListener( + "pageshow", + async () => { + await this.initialized; + try { + this._initListEventHandlers(); + this._loadData(); + await this._rebuildVisibleTypes(); + await this._rebuildView(); + await this._sortListView(); + resolve(); + } catch (ex) { + reject(ex); + } + }, + { once: true } + ); + }); + }, + + handleSubcategory(subcategory) { + if (Services.policies && !Services.policies.isAllowed("profileImport")) { + return false; + } + if (subcategory == "migrate") { + this.showMigrationWizardDialog(); + return true; + } + + if (subcategory == "migrate-autoclose") { + this.showMigrationWizardDialog({ closeTabWhenDone: true }); + } + + return false; + }, + + // CONTAINERS + + /* + * preferences: + * + * privacy.userContext.enabled + * - true if containers is enabled + */ + + /** + * Enables/disables the Settings button used to configure containers + */ + readBrowserContainersCheckbox() { + const pref = Preferences.get("privacy.userContext.enabled"); + const settings = document.getElementById("browserContainersSettings"); + + settings.disabled = !pref.value; + const containersEnabled = Services.prefs.getBoolPref( + "privacy.userContext.enabled" + ); + const containersCheckbox = document.getElementById( + "browserContainersCheckbox" + ); + containersCheckbox.checked = containersEnabled; + handleControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY).then( + isControlled => { + containersCheckbox.disabled = isControlled; + } + ); + }, + + /** + * Show the Containers UI depending on the privacy.userContext.ui.enabled pref. + */ + initBrowserContainers() { + if (!Services.prefs.getBoolPref("privacy.userContext.ui.enabled")) { + // The browserContainersGroup element has its own internal padding that + // is visible even if the browserContainersbox is visible, so hide the whole + // groupbox if the feature is disabled to prevent a gap in the preferences. + document + .getElementById("browserContainersbox") + .setAttribute("data-hidden-from-search", "true"); + return; + } + Services.prefs.addObserver(PREF_CONTAINERS_EXTENSION, this); + + document.getElementById("browserContainersbox").hidden = false; + this.readBrowserContainersCheckbox(); + }, + + async onGetStarted(aEvent) { + if (!AppConstants.MOZ_DEV_EDITION) { + return; + } + const win = Services.wm.getMostRecentWindow("navigator:browser"); + if (!win) { + return; + } + const user = await fxAccounts.getSignedInUser(); + if (user) { + // We have a user, open Sync preferences in the same tab + win.openTrustedLinkIn("about:preferences#sync", "current"); + return; + } + if (!(await FxAccounts.canConnectAccount())) { + return; + } + let url = await FxAccounts.config.promiseConnectAccountURI( + "dev-edition-setup" + ); + let accountsTab = win.gBrowser.addWebTab(url); + win.gBrowser.selectedTab = accountsTab; + }, + + // HOME PAGE + /* + * Preferences: + * + * browser.startup.page + * - what page(s) to show when the user starts the application, as an integer: + * + * 0: a blank page (DEPRECATED - this can be set via browser.startup.homepage) + * 1: the home page (as set by the browser.startup.homepage pref) + * 2: the last page the user visited (DEPRECATED) + * 3: windows and tabs from the last session (a.k.a. session restore) + * + * The deprecated option is not exposed in UI; however, if the user has it + * selected and doesn't change the UI for this preference, the deprecated + * option is preserved. + */ + + /** + * Utility function to enable/disable the button specified by aButtonID based + * on the value of the Boolean preference specified by aPreferenceID. + */ + updateButtons(aButtonID, aPreferenceID) { + var button = document.getElementById(aButtonID); + var preference = Preferences.get(aPreferenceID); + button.disabled = !preference.value; + return undefined; + }, + + /** + * Hide/show the "Show my windows and tabs from last time" option based + * on the value of the browser.privatebrowsing.autostart pref. + */ + updateBrowserStartupUI() { + const pbAutoStartPref = Preferences.get( + "browser.privatebrowsing.autostart" + ); + const startupPref = Preferences.get("browser.startup.page"); + + let newValue; + let checkbox = document.getElementById("browserRestoreSession"); + checkbox.disabled = pbAutoStartPref.value || startupPref.locked; + newValue = pbAutoStartPref.value + ? false + : startupPref.value === this.STARTUP_PREF_RESTORE_SESSION; + if (checkbox.checked !== newValue) { + checkbox.checked = newValue; + } + }, + /** + * Fetch the existing default zoom value, initialise and unhide + * the preferences menu. This method also establishes a listener + * to ensure handleDefaultZoomChange is called on future menu + * changes. + */ + async initDefaultZoomValues() { + let win = window.browsingContext.topChromeWindow; + let selected = await win.ZoomUI.getGlobalValue(); + let menulist = document.getElementById("defaultZoom"); + + new SelectionChangedMenulist(menulist, event => { + let parsedZoom = parseFloat((event.target.value / 100).toFixed(2)); + gMainPane.handleDefaultZoomChange(parsedZoom); + }); + + setEventListener("zoomText", "command", function () { + win.ZoomManager.toggleZoom(); + }); + + let zoomValues = win.ZoomManager.zoomValues.map(a => { + return Math.round(a * 100); + }); + + let fragment = document.createDocumentFragment(); + for (let zoomLevel of zoomValues) { + let menuitem = document.createXULElement("menuitem"); + document.l10n.setAttributes(menuitem, "preferences-default-zoom-value", { + percentage: zoomLevel, + }); + menuitem.setAttribute("value", zoomLevel); + fragment.appendChild(menuitem); + } + + let menupopup = menulist.querySelector("menupopup"); + menupopup.appendChild(fragment); + menulist.value = Math.round(selected * 100); + + let checkbox = document.getElementById("zoomText"); + checkbox.checked = !win.ZoomManager.useFullZoom; + + document.getElementById("zoomBox").hidden = false; + }, + + /** + * Initialize the translations view. + */ + async initTranslations() { + if (!Services.prefs.getBoolPref("browser.translations.enable")) { + return; + } + + /** + * Which phase a language download is in. + * + * @typedef {"downloaded" | "loading" | "uninstalled"} DownloadPhase + */ + + // Immediately show the group so that the async load of the component does + // not cause the layout to jump. The group will be empty initially. + document.getElementById("translationsGroup").hidden = false; + + class TranslationsState { + /** + * The fully initialized state. + * + * @param {TranslationsActor} translationsActor + * @param {Object} supportedLanguages + * @param {Array<{ langTag: string, displayName: string}} languageList + * @param {Map<string, DownloadPhase>} downloadPhases + */ + constructor( + translationsActor, + supportedLanguages, + languageList, + downloadPhases + ) { + this.translationsActor = translationsActor; + this.supportedLanguages = supportedLanguages; + this.languageList = languageList; + this.downloadPhases = downloadPhases; + } + + /** + * Handles all of the async initialization logic. + */ + static async create() { + const translationsActor = + window.windowGlobalChild.getActor("Translations"); + const supportedLanguages = + await translationsActor.getSupportedLanguages(); + const languageList = + TranslationsState.getLanguageList(supportedLanguages); + const downloadPhases = await TranslationsState.createDownloadPhases( + translationsActor, + languageList + ); + + if (supportedLanguages.languagePairs.length === 0) { + throw new Error( + "The supported languages list was empty. RemoteSettings may not be available at the moment." + ); + } + + return new TranslationsState( + translationsActor, + supportedLanguages, + languageList, + downloadPhases + ); + } + + /** + * Create a unique list of languages, sorted by the display name. + * + * @param {Object} supportedLanguages + * @returns {Array<{ langTag: string, displayName: string}} + */ + static getLanguageList(supportedLanguages) { + const displayNames = new Map(); + for (const languages of [ + supportedLanguages.fromLanguages, + supportedLanguages.toLanguages, + ]) { + for (const { langTag, displayName } of languages) { + displayNames.set(langTag, displayName); + } + } + + let appLangTag = new Intl.Locale(Services.locale.appLocaleAsBCP47) + .language; + + // Don't offer to download the app's language. + displayNames.delete(appLangTag); + + // Sort the list of languages by the display names. + return [...displayNames.entries()] + .map(([langTag, displayName]) => ({ + langTag, + displayName, + })) + .sort((a, b) => a.displayName.localeCompare(b.displayName)); + } + + /** + * Determine the download phase of each language file. + * + * @param {TranslationsChild} translationsActor + * @param {Array<{ langTag: string, displayName: string}} languageList. + * @returns {Map<string, DownloadPhase>} Map the language tag to whether it is downloaded. + */ + static async createDownloadPhases(translationsActor, languageList) { + const downloadPhases = new Map(); + for (const { langTag } of languageList) { + downloadPhases.set( + langTag, + (await translationsActor.hasAllFilesForLanguage(langTag)) + ? "downloaded" + : "uninstalled" + ); + } + return downloadPhases; + } + } + + class TranslationsView { + /** @type {Map<string, XULButton>} */ + deleteButtons = new Map(); + /** @type {Map<string, XULButton>} */ + downloadButtons = new Map(); + + /** + * @param {TranslationsState} state + */ + constructor(state) { + this.state = state; + this.elements = { + settingsButton: document.getElementById( + "translations-manage-settings-button" + ), + installList: document.getElementById( + "translations-manage-install-list" + ), + installAll: document.getElementById( + "translations-manage-install-all" + ), + deleteAll: document.getElementById("translations-manage-delete-all"), + error: document.getElementById("translations-manage-error"), + }; + this.setup(); + } + + setup() { + this.buildLanguageList(); + + this.elements.settingsButton.addEventListener( + "command", + gMainPane.showTranslationsSettings + ); + this.elements.installAll.addEventListener( + "command", + this.handleInstallAll + ); + this.elements.deleteAll.addEventListener( + "command", + this.handleDeleteAll + ); + } + + handleInstallAll = async () => { + this.hideError(); + this.disableButtons(true); + try { + await this.state.translationsActor.downloadAllFiles(); + this.markAllDownloadPhases("downloaded"); + } catch (error) { + TranslationsView.showError( + "translations-manage-error-download", + error + ); + await this.reloadDownloadPhases(); + this.updateAllButtons(); + } + this.disableButtons(false); + }; + + handleDeleteAll = async () => { + this.hideError(); + this.disableButtons(true); + try { + await this.state.translationsActor.deleteAllLanguageFiles(); + this.markAllDownloadPhases("uninstalled"); + } catch (error) { + TranslationsView.showError("translations-manage-error-delete", error); + // The download phases are invalidated with the error and must be reloaded. + await this.reloadDownloadPhases(); + console.error(error); + } + this.disableButtons(false); + }; + + /** + * @param {string} langTag + * @returns {Function} + */ + getDownloadButtonHandler(langTag) { + return async () => { + this.hideError(); + this.updateDownloadPhase(langTag, "loading"); + try { + await this.state.translationsActor.downloadLanguageFiles(langTag); + this.updateDownloadPhase(langTag, "downloaded"); + } catch (error) { + TranslationsView.showError( + "translations-manage-error-download", + error + ); + this.updateDownloadPhase(langTag, "uninstalled"); + } + }; + } + + /** + * @param {string} langTag + * @returns {Function} + */ + getDeleteButtonHandler(langTag) { + return async () => { + this.hideError(); + this.updateDownloadPhase(langTag, "loading"); + try { + await this.state.translationsActor.deleteLanguageFiles(langTag); + this.updateDownloadPhase(langTag, "uninstalled"); + } catch (error) { + TranslationsView.showError( + "translations-manage-error-delete", + error + ); + // The download phases are invalidated with the error and must be reloaded. + await this.reloadDownloadPhases(); + } + }; + } + + buildLanguageList() { + const listFragment = document.createDocumentFragment(); + + for (const { langTag, displayName } of this.state.languageList) { + const hboxRow = document.createXULElement("hbox"); + hboxRow.classList.add("translations-manage-language"); + + const languageLabel = document.createXULElement("label"); + languageLabel.textContent = displayName; // The display name is already localized. + + const downloadButton = document.createXULElement("button"); + const deleteButton = document.createXULElement("button"); + + downloadButton.addEventListener( + "command", + this.getDownloadButtonHandler(langTag) + ); + deleteButton.addEventListener( + "command", + this.getDeleteButtonHandler(langTag) + ); + + document.l10n.setAttributes( + downloadButton, + "translations-manage-download-button" + ); + document.l10n.setAttributes( + deleteButton, + "translations-manage-delete-button" + ); + + downloadButton.hidden = true; + deleteButton.hidden = true; + + this.deleteButtons.set(langTag, deleteButton); + this.downloadButtons.set(langTag, downloadButton); + + hboxRow.appendChild(languageLabel); + hboxRow.appendChild(downloadButton); + hboxRow.appendChild(deleteButton); + listFragment.appendChild(hboxRow); + } + this.updateAllButtons(); + this.elements.installList.appendChild(listFragment); + this.elements.installList.hidden = false; + } + + /** + * Update the DownloadPhase for a single langTag. + * @param {string} langTag + * @param {DownloadPhase} downloadPhase + */ + updateDownloadPhase(langTag, downloadPhase) { + this.state.downloadPhases.set(langTag, downloadPhase); + this.updateButton(langTag, downloadPhase); + this.updateHeaderButtons(); + } + + /** + * Recreates the download map when the state is invalidated. + */ + async reloadDownloadPhases() { + this.state.downloadPhases = + await TranslationsState.createDownloadPhases( + this.state.translationsActor, + this.state.languageList + ); + this.updateAllButtons(); + } + + /** + * Set all the downloads. + * @param {DownloadPhase} downloadPhase + */ + markAllDownloadPhases(downloadPhase) { + const { downloadPhases } = this.state; + for (const key of downloadPhases.keys()) { + downloadPhases.set(key, downloadPhase); + } + this.updateAllButtons(); + } + + /** + * If all languages are downloaded, or no languages are downloaded then + * the visibility of the buttons need to change. + */ + updateHeaderButtons() { + let allDownloaded = true; + let allUninstalled = true; + for (const downloadPhase of this.state.downloadPhases.values()) { + if (downloadPhase === "loading") { + // Don't count loading towards this calculation. + continue; + } + allDownloaded &&= downloadPhase === "downloaded"; + allUninstalled &&= downloadPhase === "uninstalled"; + } + + this.elements.installAll.hidden = allDownloaded; + this.elements.deleteAll.hidden = allUninstalled; + } + + /** + * Update the buttons according to their download state. + */ + updateAllButtons() { + this.updateHeaderButtons(); + for (const [langTag, downloadPhase] of this.state.downloadPhases) { + this.updateButton(langTag, downloadPhase); + } + } + + /** + * @param {string} langTag + * @param {DownloadPhase} downloadPhase + */ + updateButton(langTag, downloadPhase) { + const downloadButton = this.downloadButtons.get(langTag); + const deleteButton = this.deleteButtons.get(langTag); + switch (downloadPhase) { + case "downloaded": + downloadButton.hidden = true; + deleteButton.hidden = false; + downloadButton.removeAttribute("disabled"); + break; + case "uninstalled": + downloadButton.hidden = false; + deleteButton.hidden = true; + downloadButton.removeAttribute("disabled"); + break; + case "loading": + downloadButton.hidden = false; + deleteButton.hidden = true; + downloadButton.setAttribute("disabled", true); + break; + } + } + + /** + * @param {boolean} isDisabled + */ + disableButtons(isDisabled) { + this.elements.installAll.disabled = isDisabled; + this.elements.deleteAll.disabled = isDisabled; + for (const button of this.downloadButtons.values()) { + button.disabled = isDisabled; + } + for (const button of this.deleteButtons.values()) { + button.disabled = isDisabled; + } + } + + /** + * This method is static in case an error happens during the creation of the + * TranslationsState. + * + * @param {string} l10nId + * @param {Error} error + */ + static showError(l10nId, error) { + console.error(error); + const errorMessage = document.getElementById( + "translations-manage-error" + ); + errorMessage.hidden = false; + document.l10n.setAttributes(errorMessage, l10nId); + } + + hideError() { + this.elements.error.hidden = true; + } + } + + TranslationsState.create().then( + state => { + new TranslationsView(state); + }, + error => { + // This error can happen when a user is not connected to the internet, or + // RemoteSettings is down for some reason. + TranslationsView.showError("translations-manage-error-list", error); + } + ); + }, + + initPrimaryBrowserLanguageUI() { + // Enable telemetry. + Services.telemetry.setEventRecordingEnabled( + "intl.ui.browserLanguage", + true + ); + + // This will register the "command" listener. + let menulist = document.getElementById("primaryBrowserLocale"); + new SelectionChangedMenulist(menulist, event => { + gMainPane.onPrimaryBrowserLanguageMenuChange(event); + }); + + gMainPane.updatePrimaryBrowserLanguageUI(Services.locale.appLocaleAsBCP47); + }, + + /** + * Update the available list of locales and select the locale that the user + * is "selecting". This could be the currently requested locale or a locale + * that the user would like to switch to after confirmation. + * + * @param {string} selected - The selected BCP 47 locale. + */ + async updatePrimaryBrowserLanguageUI(selected) { + let available = await LangPackMatcher.getAvailableLocales(); + let localeNames = Services.intl.getLocaleDisplayNames( + undefined, + available, + { preferNative: true } + ); + let locales = available.map((code, i) => ({ code, name: localeNames[i] })); + locales.sort((a, b) => a.name > b.name); + + let fragment = document.createDocumentFragment(); + for (let { code, name } of locales) { + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("value", code); + menuitem.setAttribute("label", name); + fragment.appendChild(menuitem); + } + + // Add an option to search for more languages if downloading is supported. + if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) { + let menuitem = document.createXULElement("menuitem"); + menuitem.id = "primaryBrowserLocaleSearch"; + menuitem.setAttribute( + "label", + await document.l10n.formatValue("browser-languages-search") + ); + menuitem.setAttribute("value", "search"); + fragment.appendChild(menuitem); + } + + let menulist = document.getElementById("primaryBrowserLocale"); + let menupopup = menulist.querySelector("menupopup"); + menupopup.textContent = ""; + menupopup.appendChild(fragment); + menulist.value = selected; + + document.getElementById("browserLanguagesBox").hidden = false; + }, + + /* Show the confirmation message bar to allow a restart into the new locales. */ + async showConfirmLanguageChangeMessageBar(locales) { + let messageBar = document.getElementById("confirmBrowserLanguage"); + + // Get the bundle for the new locale. + let newBundle = getBundleForLocales(locales); + + // Find the messages and labels. + let messages = await Promise.all( + [newBundle, document.l10n].map(async bundle => + bundle.formatValue("confirm-browser-language-change-description") + ) + ); + let buttonLabels = await Promise.all( + [newBundle, document.l10n].map(async bundle => + bundle.formatValue("confirm-browser-language-change-button") + ) + ); + + // If both the message and label are the same, just include one row. + if (messages[0] == messages[1] && buttonLabels[0] == buttonLabels[1]) { + messages.pop(); + buttonLabels.pop(); + } + + let contentContainer = messageBar.querySelector( + ".message-bar-content-container" + ); + contentContainer.textContent = ""; + + for (let i = 0; i < messages.length; i++) { + let messageContainer = document.createXULElement("hbox"); + messageContainer.classList.add("message-bar-content"); + messageContainer.style.flex = "1 50%"; + messageContainer.setAttribute("align", "center"); + + let description = document.createXULElement("description"); + description.classList.add("message-bar-description"); + + if (i == 0 && Services.intl.getScriptDirection(locales[0]) === "rtl") { + description.classList.add("rtl-locale"); + } + description.setAttribute("flex", "1"); + description.textContent = messages[i]; + messageContainer.appendChild(description); + + let button = document.createXULElement("button"); + button.addEventListener( + "command", + gMainPane.confirmBrowserLanguageChange + ); + button.classList.add("message-bar-button"); + button.setAttribute("locales", locales.join(",")); + button.setAttribute("label", buttonLabels[i]); + messageContainer.appendChild(button); + + contentContainer.appendChild(messageContainer); + } + + messageBar.hidden = false; + gMainPane.selectedLocalesForRestart = locales; + }, + + hideConfirmLanguageChangeMessageBar() { + let messageBar = document.getElementById("confirmBrowserLanguage"); + messageBar.hidden = true; + let contentContainer = messageBar.querySelector( + ".message-bar-content-container" + ); + contentContainer.textContent = ""; + gMainPane.requestingLocales = null; + }, + + /* Confirm the locale change and restart the browser in the new locale. */ + confirmBrowserLanguageChange(event) { + let localesString = (event.target.getAttribute("locales") || "").trim(); + if (!localesString || !localesString.length) { + return; + } + let locales = localesString.split(","); + Services.locale.requestedLocales = locales; + + // Record the change in telemetry before we restart. + gMainPane.recordBrowserLanguagesTelemetry("apply"); + + // Restart with the new locale. + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + if (!cancelQuit.data) { + Services.startup.quit( + Services.startup.eAttemptQuit | Services.startup.eRestart + ); + } + }, + + /* Show or hide the confirm change message bar based on the new locale. */ + onPrimaryBrowserLanguageMenuChange(event) { + let locale = event.target.value; + + if (locale == "search") { + gMainPane.showBrowserLanguagesSubDialog({ search: true }); + return; + } else if (locale == Services.locale.appLocaleAsBCP47) { + this.hideConfirmLanguageChangeMessageBar(); + return; + } + + let newLocales = Array.from( + new Set([locale, ...Services.locale.requestedLocales]).values() + ); + + gMainPane.recordBrowserLanguagesTelemetry("reorder"); + + switch (gMainPane.getLanguageSwitchTransitionType(newLocales)) { + case "requires-restart": + // Prepare to change the locales, as they were different. + gMainPane.showConfirmLanguageChangeMessageBar(newLocales); + gMainPane.updatePrimaryBrowserLanguageUI(newLocales[0]); + break; + case "live-reload": + Services.locale.requestedLocales = newLocales; + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + case "locales-match": + // They matched, so we can reset the UI. + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + default: + throw new Error("Unhandled transition type."); + } + }, + + /** + * Takes as newZoom a floating point value representing the + * new default zoom. This value should not be a string, and + * should not carry a percentage sign/other localisation + * characteristics. + */ + handleDefaultZoomChange(newZoom) { + let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + let nonPrivateLoadContext = Cu.createLoadContext(); + /* Because our setGlobal function takes in a browsing context, and + * because we want to keep this property consistent across both private + * and non-private contexts, we crate a non-private context and use that + * to set the property, regardless of our actual context. + */ + + let win = window.browsingContext.topChromeWindow; + cps2.setGlobal(win.FullZoom.name, newZoom, nonPrivateLoadContext); + }, + + onBrowserRestoreSessionChange(event) { + const value = event.target.checked; + const startupPref = Preferences.get("browser.startup.page"); + let newValue; + + if (value) { + // We need to restore the blank homepage setting in our other pref + if (startupPref.value === this.STARTUP_PREF_BLANK) { + HomePage.safeSet("about:blank"); + } + newValue = this.STARTUP_PREF_RESTORE_SESSION; + } else { + newValue = this.STARTUP_PREF_HOMEPAGE; + } + startupPref.value = newValue; + }, + + // TABS + + /* + * Preferences: + * + * browser.link.open_newwindow - int + * Determines where links targeting new windows should open. + * Values: + * 1 - Open in the current window or tab. + * 2 - Open in a new window. + * 3 - Open in a new tab in the most recent window. + * browser.tabs.loadInBackground - bool + * True - Whether browser should switch to a new tab opened from a link. + * browser.tabs.warnOnClose - bool + * True - If when closing a window with multiple tabs the user is warned and + * allowed to cancel the action, false to just close the window. + * browser.warnOnQuitShortcut - bool + * True - If the keyboard shortcut (Ctrl/Cmd+Q) is pressed, the user should + * be warned, false to just quit without prompting. + * browser.tabs.warnOnOpen - bool + * True - Whether the user should be warned when trying to open a lot of + * tabs at once (e.g. a large folder of bookmarks), allowing to + * cancel the action. + * browser.taskbar.previews.enable - bool + * True - Tabs are to be shown in Windows 7 taskbar. + * False - Only the window is to be shown in Windows 7 taskbar. + */ + + /** + * Determines where a link which opens a new window will open. + * + * @returns |true| if such links should be opened in new tabs + */ + readLinkTarget() { + var openNewWindow = Preferences.get("browser.link.open_newwindow"); + return openNewWindow.value != 2; + }, + + /** + * Determines where a link which opens a new window will open. + * + * @returns 2 if such links should be opened in new windows, + * 3 if such links should be opened in new tabs + */ + writeLinkTarget() { + var linkTargeting = document.getElementById("linkTargeting"); + return linkTargeting.checked ? 3 : 2; + }, + /* + * Preferences: + * + * browser.shell.checkDefault + * - true if a default-browser check (and prompt to make it so if necessary) + * occurs at startup, false otherwise + */ + + /** + * Show button for setting browser as default browser or information that + * browser is already the default browser. + */ + updateSetDefaultBrowser() { + if (AppConstants.HAVE_SHELL_SERVICE) { + let shellSvc = getShellService(); + let defaultBrowserBox = document.getElementById("defaultBrowserBox"); + let isInFlatpak = gGIOService?.isRunningUnderFlatpak; + // Flatpak does not support setting nor detection of default browser + if (!shellSvc || isInFlatpak) { + defaultBrowserBox.hidden = true; + return; + } + let isDefault = shellSvc.isDefaultBrowser(false, true); + let setDefaultPane = document.getElementById("setDefaultPane"); + setDefaultPane.classList.toggle("is-default", isDefault); + let alwaysCheck = document.getElementById("alwaysCheckDefault"); + let alwaysCheckPref = Preferences.get( + "browser.shell.checkDefaultBrowser" + ); + alwaysCheck.disabled = alwaysCheckPref.locked || isDefault; + } + }, + + /** + * Set browser as the operating system default browser. + */ + setDefaultBrowser() { + if (AppConstants.HAVE_SHELL_SERVICE) { + let alwaysCheckPref = Preferences.get( + "browser.shell.checkDefaultBrowser" + ); + alwaysCheckPref.value = true; + + // Reset exponential backoff delay time in order to do visual update in pollForDefaultBrowser. + this._backoffIndex = 0; + + let shellSvc = getShellService(); + if (!shellSvc) { + return; + } + try { + shellSvc.setDefaultBrowser(true, false); + } catch (ex) { + console.error(ex); + return; + } + + let isDefault = shellSvc.isDefaultBrowser(false, true); + let setDefaultPane = document.getElementById("setDefaultPane"); + setDefaultPane.classList.toggle("is-default", isDefault); + } + }, + + /** + * Shows a dialog in which the preferred language for web content may be set. + */ + showLanguages() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/languages.xhtml" + ); + }, + + recordBrowserLanguagesTelemetry(method, value = null) { + Services.telemetry.recordEvent( + "intl.ui.browserLanguage", + method, + "main", + value + ); + }, + + /** + * Open the browser languages sub dialog in either the normal mode, or search mode. + * The search mode is only available from the menu to change the primary browser + * language. + * + * @param {{ search: boolean }} + */ + showBrowserLanguagesSubDialog({ search }) { + // Record the telemetry event with an id to associate related actions. + let telemetryId = parseInt( + Services.telemetry.msSinceProcessStart(), + 10 + ).toString(); + let method = search ? "search" : "manage"; + gMainPane.recordBrowserLanguagesTelemetry(method, telemetryId); + + let opts = { + selectedLocalesForRestart: gMainPane.selectedLocalesForRestart, + search, + telemetryId, + }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml", + { closingCallback: this.browserLanguagesClosed }, + opts + ); + }, + + /** + * Determine the transition strategy for switching the locale based on prefs + * and the switched locales. + * + * @param {Array<string>} newLocales - List of BCP 47 locale identifiers. + * @returns {"locales-match" | "requires-restart" | "live-reload"} + */ + getLanguageSwitchTransitionType(newLocales) { + const { appLocalesAsBCP47 } = Services.locale; + if (appLocalesAsBCP47.join(",") === newLocales.join(",")) { + // The selected locales match, the order matters. + return "locales-match"; + } + + if (Services.prefs.getBoolPref("intl.multilingual.liveReload")) { + if ( + Services.intl.getScriptDirection(newLocales[0]) !== + Services.intl.getScriptDirection(appLocalesAsBCP47[0]) && + !Services.prefs.getBoolPref("intl.multilingual.liveReloadBidirectional") + ) { + // Bug 1750852: The directionality of the text changed, which requires a restart + // until the quality of the switch can be improved. + return "requires-restart"; + } + + return "live-reload"; + } + + return "requires-restart"; + }, + + /* Show or hide the confirm change message bar based on the updated ordering. */ + browserLanguagesClosed() { + // When the subdialog is closed, settings are stored on gBrowserLanguagesDialog. + // The next time the dialog is opened, a new gBrowserLanguagesDialog is created. + let { selected } = this.gBrowserLanguagesDialog; + + this.gBrowserLanguagesDialog.recordTelemetry( + selected ? "accept" : "cancel" + ); + + if (!selected) { + // No locales were selected. Cancel the operation. + return; + } + + switch (gMainPane.getLanguageSwitchTransitionType(selected)) { + case "requires-restart": + gMainPane.showConfirmLanguageChangeMessageBar(selected); + gMainPane.updatePrimaryBrowserLanguageUI(selected[0]); + break; + case "live-reload": + Services.locale.requestedLocales = selected; + + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + case "locales-match": + // They matched, so we can reset the UI. + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + default: + throw new Error("Unhandled transition type."); + } + }, + + displayUseSystemLocale() { + let appLocale = Services.locale.appLocaleAsBCP47; + let regionalPrefsLocales = Services.locale.regionalPrefsLocales; + if (!regionalPrefsLocales.length) { + return; + } + let systemLocale = regionalPrefsLocales[0]; + let localeDisplayname = Services.intl.getLocaleDisplayNames( + undefined, + [systemLocale], + { preferNative: true } + ); + if (!localeDisplayname.length) { + return; + } + let localeName = localeDisplayname[0]; + if (appLocale.split("-u-")[0] != systemLocale.split("-u-")[0]) { + let checkbox = document.getElementById("useSystemLocale"); + document.l10n.setAttributes(checkbox, "use-system-locale", { + localeName, + }); + checkbox.hidden = false; + } + }, + + /** + * Displays the translation exceptions dialog where specific site and language + * translation preferences can be set. + */ + // TODO (Bug 1817084) Remove this code when we disable the extension + showTranslationExceptions() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/translationExceptions.xhtml" + ); + }, + + showTranslationsSettings() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/translations.xhtml" + ); + }, + + openTranslationProviderAttribution() { + var { Translation } = ChromeUtils.import( + "resource:///modules/translation/TranslationParent.jsm" + ); + Translation.openProviderAttribution(); + }, + + /** + * Displays the fonts dialog, where web page font names and sizes can be + * configured. + */ + configureFonts() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/fonts.xhtml", + { features: "resizable=no" } + ); + }, + + /** + * Displays the colors dialog, where default web page/link/etc. colors can be + * configured. + */ + configureColors() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/colors.xhtml", + { features: "resizable=no" } + ); + }, + + // NETWORK + /** + * Displays a dialog in which proxy settings may be changed. + */ + showConnections() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/connection.xhtml", + { closingCallback: this.updateProxySettingsUI.bind(this) } + ); + }, + + // Update the UI to show the proper description depending on whether an + // extension is in control or not. + async updateProxySettingsUI() { + let controllingExtension = await getControllingExtension( + PREF_SETTING_TYPE, + PROXY_KEY + ); + let description = document.getElementById("connectionSettingsDescription"); + + if (controllingExtension) { + setControllingExtensionDescription( + description, + controllingExtension, + "proxy.settings" + ); + } else { + setControllingExtensionDescription( + description, + null, + "network-proxy-connection-description" + ); + } + }, + + async checkBrowserContainers(event) { + let checkbox = document.getElementById("browserContainersCheckbox"); + if (checkbox.checked) { + Services.prefs.setBoolPref("privacy.userContext.enabled", true); + return; + } + + let count = ContextualIdentityService.countContainerTabs(); + if (count == 0) { + Services.prefs.setBoolPref("privacy.userContext.enabled", false); + return; + } + + let [title, message, okButton, cancelButton] = + await document.l10n.formatValues([ + { id: "containers-disable-alert-title" }, + { id: "containers-disable-alert-desc", args: { tabCount: count } }, + { id: "containers-disable-alert-ok-button", args: { tabCount: count } }, + { id: "containers-disable-alert-cancel-button" }, + ]); + + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1; + + let rv = Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + okButton, + cancelButton, + null, + null, + {} + ); + if (rv == 0) { + Services.prefs.setBoolPref("privacy.userContext.enabled", false); + return; + } + + checkbox.checked = true; + }, + + /** + * Displays container panel for customising and adding containers. + */ + showContainerSettings() { + gotoPref("containers"); + }, + + /** + * ui.osk.enabled + * - when set to true, subject to other conditions, we may sometimes invoke + * an on-screen keyboard when a text input is focused. + * (Currently Windows-only, and depending on prefs, may be Windows-8-only) + */ + updateOnScreenKeyboardVisibility() { + if (AppConstants.platform == "win") { + let minVersion = Services.prefs.getBoolPref("ui.osk.require_win10") + ? 10 + : 6.2; + if ( + Services.vc.compare( + Services.sysinfo.getProperty("version"), + minVersion + ) >= 0 + ) { + document.getElementById("useOnScreenKeyboard").hidden = false; + } + } + }, + + updateHardwareAcceleration() { + // Placeholder for restart on change + }, + + // FONTS + + /** + * Populates the default font list in UI. + */ + _rebuildFonts() { + var langGroupPref = Preferences.get("font.language.group"); + var isSerif = + this._readDefaultFontTypeForLanguage(langGroupPref.value) == "serif"; + this._selectDefaultLanguageGroup(langGroupPref.value, isSerif); + }, + + /** + * Returns the type of the current default font for the language denoted by + * aLanguageGroup. + */ + _readDefaultFontTypeForLanguage(aLanguageGroup) { + const kDefaultFontType = "font.default.%LANG%"; + var defaultFontTypePref = kDefaultFontType.replace( + /%LANG%/, + aLanguageGroup + ); + var preference = Preferences.get(defaultFontTypePref); + if (!preference) { + preference = Preferences.add({ id: defaultFontTypePref, type: "string" }); + preference.on("change", gMainPane._rebuildFonts.bind(gMainPane)); + } + return preference.value; + }, + + _selectDefaultLanguageGroupPromise: Promise.resolve(), + + _selectDefaultLanguageGroup(aLanguageGroup, aIsSerif) { + this._selectDefaultLanguageGroupPromise = (async () => { + // Avoid overlapping language group selections by awaiting the resolution + // of the previous one. We do this because this function is re-entrant, + // as inserting <preference> elements into the DOM sometimes triggers a call + // back into this function. And since this function is also asynchronous, + // that call can enter this function before the previous run has completed, + // which would corrupt the font menulists. Awaiting the previous call's + // resolution avoids that fate. + await this._selectDefaultLanguageGroupPromise; + + const kFontNameFmtSerif = "font.name.serif.%LANG%"; + const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%"; + const kFontNameListFmtSerif = "font.name-list.serif.%LANG%"; + const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%"; + const kFontSizeFmtVariable = "font.size.variable.%LANG%"; + + var prefs = [ + { + format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif, + type: "fontname", + element: "defaultFont", + fonttype: aIsSerif ? "serif" : "sans-serif", + }, + { + format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif, + type: "unichar", + element: null, + fonttype: aIsSerif ? "serif" : "sans-serif", + }, + { + format: kFontSizeFmtVariable, + type: "int", + element: "defaultFontSize", + fonttype: null, + }, + ]; + for (var i = 0; i < prefs.length; ++i) { + var preference = Preferences.get( + prefs[i].format.replace(/%LANG%/, aLanguageGroup) + ); + if (!preference) { + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + preference = Preferences.add({ id: name, type: prefs[i].type }); + } + + if (!prefs[i].element) { + continue; + } + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) { + await FontBuilder.buildFontList( + aLanguageGroup, + prefs[i].fonttype, + element + ); + } + + preference.setElementValue(element); + } + } + })().catch(console.error); + }, + + onMigrationButtonCommand(command) { + // When browser.migrate.content-modal.enabled is enabled by default, + // the event handler can just call showMigrationWizardDialog directly, + // but for now, we delegate to MigrationUtils to open the native modal + // in case that's the dialog we're still using. + // + // Enabling the pref by default will be part of bug 1822156. + const browser = window.docShell.chromeEventHandler; + const browserWindow = browser.ownerGlobal; + + // showMigrationWizard blocks on some platforms. We'll dispatch the request + // to open to a runnable on the main thread so that we don't have to block + // this function call. + Services.tm.dispatchToMainThread(() => { + MigrationUtils.showMigrationWizard(browserWindow, { + entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES, + }); + }); + }, + + /** + * Displays the migration wizard dialog in an HTML dialog. + */ + async showMigrationWizardDialog({ closeTabWhenDone = false } = {}) { + let migrationWizardDialog = document.getElementById( + "migrationWizardDialog" + ); + + if (migrationWizardDialog.open) { + return; + } + + await customElements.whenDefined("migration-wizard"); + + // If we've been opened before, remove the old wizard and insert a + // new one to put it back into its starting state. + if (!migrationWizardDialog.firstElementChild) { + let wizard = document.createElement("migration-wizard"); + wizard.toggleAttribute("dialog-mode", true); + + let panelList = document.createElement("panel-list"); + let panel = document.createXULElement("panel"); + panel.appendChild(panelList); + wizard.appendChild(panel); + + migrationWizardDialog.appendChild(wizard); + } + migrationWizardDialog.firstElementChild.requestState(); + + migrationWizardDialog.addEventListener( + "close", + () => { + // Let others know that the wizard is closed -- potentially because of a + // user action within the dialog that dispatches "MigrationWizard:Close" + // but this also covers cases like hitting Escape. + Services.obs.notifyObservers( + migrationWizardDialog, + "MigrationWizard:Closed" + ); + if (closeTabWhenDone) { + window.close(); + } + }, + { once: true } + ); + + migrationWizardDialog.showModal(); + }, + + /** + * Stores the original value of the spellchecking preference to enable proper + * restoration if unchanged (since we're mapping a tristate onto a checkbox). + */ + _storedSpellCheck: 0, + + /** + * Returns true if any spellchecking is enabled and false otherwise, caching + * the current value to enable proper pref restoration if the checkbox is + * never changed. + * + * layout.spellcheckDefault + * - an integer: + * 0 disables spellchecking + * 1 enables spellchecking, but only for multiline text fields + * 2 enables spellchecking for all text fields + */ + readCheckSpelling() { + var pref = Preferences.get("layout.spellcheckDefault"); + this._storedSpellCheck = pref.value; + + return pref.value != 0; + }, + + /** + * Returns the value of the spellchecking preference represented by UI, + * preserving the preference's "hidden" value if the preference is + * unchanged and represents a value not strictly allowed in UI. + */ + writeCheckSpelling() { + var checkbox = document.getElementById("checkSpelling"); + if (checkbox.checked) { + if (this._storedSpellCheck == 2) { + return 2; + } + return 1; + } + return 0; + }, + + updateDefaultPerformanceSettingsPref() { + let defaultPerformancePref = Preferences.get( + "browser.preferences.defaultPerformanceSettings.enabled" + ); + let processCountPref = Preferences.get("dom.ipc.processCount"); + let accelerationPref = Preferences.get("layers.acceleration.disabled"); + if ( + processCountPref.value != processCountPref.defaultValue || + accelerationPref.value != accelerationPref.defaultValue + ) { + defaultPerformancePref.value = false; + } + }, + + updatePerformanceSettingsBox({ duringChangeEvent }) { + let defaultPerformancePref = Preferences.get( + "browser.preferences.defaultPerformanceSettings.enabled" + ); + let performanceSettings = document.getElementById("performanceSettings"); + let processCountPref = Preferences.get("dom.ipc.processCount"); + if (defaultPerformancePref.value) { + let accelerationPref = Preferences.get("layers.acceleration.disabled"); + // Unset the value so process count will be decided by the platform. + processCountPref.value = processCountPref.defaultValue; + accelerationPref.value = accelerationPref.defaultValue; + performanceSettings.hidden = true; + } else { + performanceSettings.hidden = false; + } + }, + + buildContentProcessCountMenuList() { + if (Services.appinfo.fissionAutostart) { + document.getElementById("limitContentProcess").hidden = true; + document.getElementById("contentProcessCount").hidden = true; + document.getElementById( + "contentProcessCountEnabledDescription" + ).hidden = true; + document.getElementById( + "contentProcessCountDisabledDescription" + ).hidden = true; + return; + } + if (Services.appinfo.browserTabsRemoteAutostart) { + let processCountPref = Preferences.get("dom.ipc.processCount"); + let defaultProcessCount = processCountPref.defaultValue; + + let contentProcessCount = + document.querySelector(`#contentProcessCount > menupopup > + menuitem[value="${defaultProcessCount}"]`); + + document.l10n.setAttributes( + contentProcessCount, + "performance-default-content-process-count", + { num: defaultProcessCount } + ); + + document.getElementById("limitContentProcess").disabled = false; + document.getElementById("contentProcessCount").disabled = false; + document.getElementById( + "contentProcessCountEnabledDescription" + ).hidden = false; + document.getElementById( + "contentProcessCountDisabledDescription" + ).hidden = true; + } else { + document.getElementById("limitContentProcess").disabled = true; + document.getElementById("contentProcessCount").disabled = true; + document.getElementById( + "contentProcessCountEnabledDescription" + ).hidden = true; + document.getElementById( + "contentProcessCountDisabledDescription" + ).hidden = false; + } + }, + + _minUpdatePrefDisableTime: 1000, + /** + * Selects the correct item in the update radio group + */ + async readUpdateAutoPref() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + + radiogroup.disabled = true; + let enabled = await UpdateUtils.getAppUpdateAutoEnabled(); + radiogroup.value = enabled; + radiogroup.disabled = false; + + this.maybeDisableBackgroundUpdateControls(); + } + }, + + /** + * Writes the value of the automatic update radio group to the disk + */ + async writeUpdateAutoPref() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + let updateAutoValue = radiogroup.value == "true"; + let _disableTimeOverPromise = new Promise(r => + setTimeout(r, this._minUpdatePrefDisableTime) + ); + radiogroup.disabled = true; + try { + await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue); + await _disableTimeOverPromise; + radiogroup.disabled = false; + } catch (error) { + console.error(error); + await Promise.all([ + this.readUpdateAutoPref(), + this.reportUpdatePrefWriteError(), + ]); + return; + } + + this.maybeDisableBackgroundUpdateControls(); + + // If the value was changed to false the user should be given the option + // to discard an update if there is one. + if (!updateAutoValue) { + await this.checkUpdateInProgress(); + } + // For tests: + radiogroup.dispatchEvent(new CustomEvent("ProcessedUpdatePrefChange")); + } + }, + + isBackgroundUpdateUIAvailable() { + return ( + AppConstants.MOZ_UPDATE_AGENT && + // This UI controls a per-installation pref. It won't necessarily work + // properly if per-installation prefs aren't supported. + UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp && + !UpdateUtils.appUpdateSettingIsLocked("app.update.background.enabled") + ); + }, + + maybeDisableBackgroundUpdateControls() { + if (this.isBackgroundUpdateUIAvailable()) { + let radiogroup = document.getElementById("updateRadioGroup"); + let updateAutoEnabled = radiogroup.value == "true"; + + // This control is only active if auto update is enabled. + document.getElementById("backgroundUpdate").disabled = !updateAutoEnabled; + } + }, + + async readBackgroundUpdatePref() { + const prefName = "app.update.background.enabled"; + if (this.isBackgroundUpdateUIAvailable()) { + let backgroundCheckbox = document.getElementById("backgroundUpdate"); + + // When the page first loads, the checkbox is unchecked until we finish + // reading the config file from the disk. But, ideally, we don't want to + // give the user the impression that this setting has somehow gotten + // turned off and they need to turn it back on. We also don't want the + // user interacting with the control, expecting a particular behavior, and + // then have the read complete and change the control in an unexpected + // way. So we disable the control while we are reading. + // The only entry points for this function are page load and user + // interaction with the control. By disabling the control to prevent + // further user interaction, we prevent the possibility of entering this + // function a second time while we are still reading. + backgroundCheckbox.disabled = true; + + // If we haven't already done this, it might result in the effective value + // of the Background Update pref changing. Thus, we should do it before + // we tell the user what value this pref has. + await BackgroundUpdate.ensureExperimentToRolloutTransitionPerformed(); + + let enabled = await UpdateUtils.readUpdateConfigSetting(prefName); + backgroundCheckbox.checked = enabled; + this.maybeDisableBackgroundUpdateControls(); + } + }, + + async writeBackgroundUpdatePref() { + const prefName = "app.update.background.enabled"; + if (this.isBackgroundUpdateUIAvailable()) { + let backgroundCheckbox = document.getElementById("backgroundUpdate"); + backgroundCheckbox.disabled = true; + let backgroundUpdateEnabled = backgroundCheckbox.checked; + try { + await UpdateUtils.writeUpdateConfigSetting( + prefName, + backgroundUpdateEnabled + ); + } catch (error) { + console.error(error); + await this.readBackgroundUpdatePref(); + await this.reportUpdatePrefWriteError(); + return; + } + + this.maybeDisableBackgroundUpdateControls(); + } + }, + + async reportUpdatePrefWriteError() { + let [title, message] = await document.l10n.formatValues([ + { id: "update-setting-write-failure-title2" }, + { + id: "update-setting-write-failure-message2", + args: { path: UpdateUtils.configFilePath }, + }, + ]); + + // Set up the Ok Button + let buttonFlags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK; + Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + null, + null, + null, + null, + {} + ); + }, + + async checkUpdateInProgress() { + let um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + if (!um.readyUpdate && !um.downloadingUpdate) { + return; + } + + let [title, message, okButton, cancelButton] = + await document.l10n.formatValues([ + { id: "update-in-progress-title" }, + { id: "update-in-progress-message" }, + { id: "update-in-progress-ok-button" }, + { id: "update-in-progress-cancel-button" }, + ]); + + // Continue is the cancel button which is BUTTON_POS_1 and is set as the + // default so pressing escape or using a platform standard method of closing + // the UI will not discard the update. + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 + + Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; + + let rv = Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + okButton, + cancelButton, + null, + null, + {} + ); + if (rv != 1) { + let aus = Cc["@mozilla.org/updates/update-service;1"].getService( + Ci.nsIApplicationUpdateService + ); + await aus.stopDownload(); + um.cleanupReadyUpdate(); + um.cleanupDownloadingUpdate(); + } + }, + + /** + * Displays the history of installed updates. + */ + showUpdates() { + gSubDialog.open("chrome://mozapps/content/update/history.xhtml"); + }, + + destroy() { + window.removeEventListener("unload", this); + Services.prefs.removeObserver(PREF_CONTAINERS_EXTENSION, this); + Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC); + Services.obs.removeObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC); + AppearanceChooser.destroy(); + }, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + // nsIObserver + + async observe(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + if (aData == PREF_CONTAINERS_EXTENSION) { + this.readBrowserContainersCheckbox(); + return; + } + // Rebuild the list when there are changes to preferences that influence + // whether or not to show certain entries in the list. + if (!this._storingAction) { + await this._rebuildView(); + } + } else if (aTopic == AUTO_UPDATE_CHANGED_TOPIC) { + if (!AppConstants.MOZ_UPDATER) { + return; + } + if (aData != "true" && aData != "false") { + throw new Error("Invalid preference value for app.update.auto"); + } + document.getElementById("updateRadioGroup").value = aData; + this.maybeDisableBackgroundUpdateControls(); + } else if (aTopic == BACKGROUND_UPDATE_CHANGED_TOPIC) { + if (!AppConstants.MOZ_UPDATE_AGENT) { + return; + } + if (aData != "true" && aData != "false") { + throw new Error( + "Invalid preference value for app.update.background.enabled" + ); + } + document.getElementById("backgroundUpdate").checked = aData == "true"; + } + }, + + // EventListener + + handleEvent(aEvent) { + if (aEvent.type == "unload") { + this.destroy(); + if (AppConstants.MOZ_UPDATER) { + onUnload(); + } + } + }, + + // Composed Model Construction + + _loadData() { + this._loadInternalHandlers(); + this._loadApplicationHandlers(); + }, + + /** + * Load higher level internal handlers so they can be turned on/off in the + * applications menu. + */ + _loadInternalHandlers() { + let internalHandlers = [new PDFHandlerInfoWrapper()]; + + let enabledHandlers = Services.prefs + .getCharPref("browser.download.viewableInternally.enabledTypes", "") + .trim(); + if (enabledHandlers) { + for (let ext of enabledHandlers.split(",")) { + internalHandlers.push( + new ViewableInternallyHandlerInfoWrapper(null, ext.trim()) + ); + } + } + for (let internalHandler of internalHandlers) { + if (internalHandler.enabled) { + this._handledTypes[internalHandler.type] = internalHandler; + } + } + }, + + /** + * Load the set of handlers defined by the application datastore. + */ + _loadApplicationHandlers() { + for (let wrappedHandlerInfo of gHandlerService.enumerate()) { + let type = wrappedHandlerInfo.type; + + let handlerInfoWrapper; + if (type in this._handledTypes) { + handlerInfoWrapper = this._handledTypes[type]; + } else { + if (DownloadIntegration.shouldViewDownloadInternally(type)) { + handlerInfoWrapper = new ViewableInternallyHandlerInfoWrapper(type); + } else { + handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo); + } + this._handledTypes[type] = handlerInfoWrapper; + } + } + }, + + // View Construction + + selectedHandlerListItem: null, + + _initListEventHandlers() { + this._list.addEventListener("select", event => { + if (event.target != this._list) { + return; + } + + let handlerListItem = + this._list.selectedItem && + HandlerListItem.forNode(this._list.selectedItem); + if (this.selectedHandlerListItem == handlerListItem) { + return; + } + + if (this.selectedHandlerListItem) { + this.selectedHandlerListItem.showActionsMenu = false; + } + this.selectedHandlerListItem = handlerListItem; + if (handlerListItem) { + this.rebuildActionsMenu(); + handlerListItem.showActionsMenu = true; + } + }); + }, + + async _rebuildVisibleTypes() { + this._visibleTypes = []; + + // Map whose keys are string descriptions and values are references to the + // first visible HandlerInfoWrapper that has this description. We use this + // to determine whether or not to annotate descriptions with their types to + // distinguish duplicate descriptions from each other. + let visibleDescriptions = new Map(); + for (let type in this._handledTypes) { + // Yield before processing each handler info object to avoid monopolizing + // the main thread, as the objects are retrieved lazily, and retrieval + // can be expensive on Windows. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + let handlerInfo = this._handledTypes[type]; + + // We couldn't find any reason to exclude the type, so include it. + this._visibleTypes.push(handlerInfo); + + let key = JSON.stringify(handlerInfo.description); + let otherHandlerInfo = visibleDescriptions.get(key); + if (!otherHandlerInfo) { + // This is the first type with this description that we encountered + // while rebuilding the _visibleTypes array this time. Make sure the + // flag is reset so we won't add the type to the description. + handlerInfo.disambiguateDescription = false; + visibleDescriptions.set(key, handlerInfo); + } else { + // There is at least another type with this description. Make sure we + // add the type to the description on both HandlerInfoWrapper objects. + handlerInfo.disambiguateDescription = true; + otherHandlerInfo.disambiguateDescription = true; + } + } + }, + + async _rebuildView() { + let lastSelectedType = + this.selectedHandlerListItem && + this.selectedHandlerListItem.handlerInfoWrapper.type; + this.selectedHandlerListItem = null; + + // Clear the list of entries. + this._list.textContent = ""; + + var visibleTypes = this._visibleTypes; + + let items = visibleTypes.map( + visibleType => new HandlerListItem(visibleType) + ); + let itemsFragment = document.createDocumentFragment(); + let lastSelectedItem; + for (let item of items) { + item.createNode(itemsFragment); + if (item.handlerInfoWrapper.type == lastSelectedType) { + lastSelectedItem = item; + } + } + + for (let item of items) { + item.setupNode(); + this.rebuildActionsMenu(item.node, item.handlerInfoWrapper); + item.refreshAction(); + } + + // If the user is filtering the list, then only show matching types. + // If we filter, we need to first localize the fragment, to + // be able to filter by localized values. + if (this._filter.value) { + await document.l10n.translateFragment(itemsFragment); + + this._filterView(itemsFragment); + + document.l10n.pauseObserving(); + this._list.appendChild(itemsFragment); + document.l10n.resumeObserving(); + } else { + // Otherwise we can just append the fragment and it'll + // get localized via the Mutation Observer. + this._list.appendChild(itemsFragment); + } + + if (lastSelectedItem) { + this._list.selectedItem = lastSelectedItem.node; + } + }, + + /** + * Whether or not the given handler app is valid. + * + * @param aHandlerApp {nsIHandlerApp} the handler app in question + * + * @returns {boolean} whether or not it's valid + */ + isValidHandlerApp(aHandlerApp) { + if (!aHandlerApp) { + return false; + } + + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) { + return this._isValidHandlerExecutable(aHandlerApp.executable); + } + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) { + return aHandlerApp.uriTemplate; + } + + if (aHandlerApp instanceof Ci.nsIGIOMimeApp) { + return aHandlerApp.command; + } + + return false; + }, + + _isValidHandlerExecutable(aExecutable) { + let leafName; + if (AppConstants.platform == "win") { + leafName = `${AppConstants.MOZ_APP_NAME}.exe`; + } else if (AppConstants.platform == "macosx") { + leafName = AppConstants.MOZ_MACBUNDLE_NAME; + } else { + leafName = `${AppConstants.MOZ_APP_NAME}-bin`; + } + return ( + aExecutable && + aExecutable.exists() && + aExecutable.isExecutable() && + // XXXben - we need to compare this with the running instance executable + // just don't know how to do that via script... + // XXXmano TBD: can probably add this to nsIShellService + aExecutable.leafName != leafName + ); + }, + + /** + * Rebuild the actions menu for the selected entry. Gets called by + * the richlistitem constructor when an entry in the list gets selected. + */ + rebuildActionsMenu( + typeItem = this._list.selectedItem, + handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper + ) { + var menu = typeItem.querySelector(".actionsMenu"); + var menuPopup = menu.menupopup; + + // Clear out existing items. + while (menuPopup.hasChildNodes()) { + menuPopup.removeChild(menuPopup.lastChild); + } + + let internalMenuItem; + // Add the "Open in Firefox" option for optional internal handlers. + if ( + handlerInfo instanceof InternalHandlerInfoWrapper && + !handlerInfo.preventInternalViewing + ) { + internalMenuItem = document.createXULElement("menuitem"); + internalMenuItem.setAttribute( + "action", + Ci.nsIHandlerInfo.handleInternally + ); + document.l10n.setAttributes(internalMenuItem, "applications-open-inapp"); + internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "handleInternally"); + menuPopup.appendChild(internalMenuItem); + } + + var askMenuItem = document.createXULElement("menuitem"); + askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk); + document.l10n.setAttributes(askMenuItem, "applications-always-ask"); + askMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask"); + menuPopup.appendChild(askMenuItem); + + // Create a menu item for saving to disk. + // Note: this option isn't available to protocol types, since we don't know + // what it means to save a URL having a certain scheme to disk. + if (handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) { + var saveMenuItem = document.createXULElement("menuitem"); + saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk); + document.l10n.setAttributes(saveMenuItem, "applications-action-save"); + saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save"); + menuPopup.appendChild(saveMenuItem); + } + + // Add a separator to distinguish these items from the helper app items + // that follow them. + let menuseparator = document.createXULElement("menuseparator"); + menuPopup.appendChild(menuseparator); + + // Create a menu item for the OS default application, if any. + if (handlerInfo.hasDefaultHandler) { + var defaultMenuItem = document.createXULElement("menuitem"); + defaultMenuItem.setAttribute( + "action", + Ci.nsIHandlerInfo.useSystemDefault + ); + // If an internal option is available, don't show the application + // name for the OS default to prevent two options from appearing + // that may both say "Firefox". + if (internalMenuItem) { + document.l10n.setAttributes( + defaultMenuItem, + "applications-use-os-default" + ); + defaultMenuItem.setAttribute("image", ICON_URL_APP); + } else { + document.l10n.setAttributes( + defaultMenuItem, + "applications-use-app-default", + { + "app-name": handlerInfo.defaultDescription, + } + ); + defaultMenuItem.setAttribute( + "image", + handlerInfo.iconURLForSystemDefault + ); + } + + menuPopup.appendChild(defaultMenuItem); + } + + // Create menu items for possible handlers. + let preferredApp = handlerInfo.preferredApplicationHandler; + var possibleAppMenuItems = []; + for (let possibleApp of handlerInfo.possibleApplicationHandlers.enumerate()) { + if (!this.isValidHandlerApp(possibleApp)) { + continue; + } + + let menuItem = document.createXULElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + let label; + if (possibleApp instanceof Ci.nsILocalHandlerApp) { + label = getFileDisplayName(possibleApp.executable); + } else { + label = possibleApp.name; + } + document.l10n.setAttributes(menuItem, "applications-use-app", { + "app-name": label, + }); + menuItem.setAttribute( + "image", + this._getIconURLForHandlerApp(possibleApp) + ); + + // Attach the handler app object to the menu item so we can use it + // to make changes to the datastore when the user selects the item. + menuItem.handlerApp = possibleApp; + + menuPopup.appendChild(menuItem); + possibleAppMenuItems.push(menuItem); + } + // Add gio handlers + if (gGIOService) { + var gioApps = gGIOService.getAppsForURIScheme(handlerInfo.type); + let possibleHandlers = handlerInfo.possibleApplicationHandlers; + for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) { + // OS handler share the same name, it's most likely the same app, skipping... + if (handler.name == handlerInfo.defaultDescription) { + continue; + } + // Check if the handler is already in possibleHandlers + let appAlreadyInHandlers = false; + for (let i = possibleHandlers.length - 1; i >= 0; --i) { + let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp); + // nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp + if (handler.equals(app)) { + appAlreadyInHandlers = true; + break; + } + } + if (!appAlreadyInHandlers) { + let menuItem = document.createXULElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + document.l10n.setAttributes(menuItem, "applications-use-app", { + "app-name": handler.name, + }); + menuItem.setAttribute( + "image", + this._getIconURLForHandlerApp(handler) + ); + + // Attach the handler app object to the menu item so we can use it + // to make changes to the datastore when the user selects the item. + menuItem.handlerApp = handler; + + menuPopup.appendChild(menuItem); + possibleAppMenuItems.push(menuItem); + } + } + } + + // Create a menu item for selecting a local application. + let canOpenWithOtherApp = true; + if (AppConstants.platform == "win") { + // On Windows, selecting an application to open another application + // would be meaningless so we special case executables. + let executableType = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromExtension("exe"); + canOpenWithOtherApp = handlerInfo.type != executableType; + } + if (canOpenWithOtherApp) { + let menuItem = document.createXULElement("menuitem"); + menuItem.className = "choose-app-item"; + menuItem.addEventListener("command", function (e) { + gMainPane.chooseApp(e); + }); + document.l10n.setAttributes(menuItem, "applications-use-other"); + menuPopup.appendChild(menuItem); + } + + // Create a menu item for managing applications. + if (possibleAppMenuItems.length) { + let menuItem = document.createXULElement("menuseparator"); + menuPopup.appendChild(menuItem); + menuItem = document.createXULElement("menuitem"); + menuItem.className = "manage-app-item"; + menuItem.addEventListener("command", function (e) { + gMainPane.manageApp(e); + }); + document.l10n.setAttributes(menuItem, "applications-manage-app"); + menuPopup.appendChild(menuItem); + } + + // Select the item corresponding to the preferred action. If the always + // ask flag is set, it overrides the preferred action. Otherwise we pick + // the item identified by the preferred action (when the preferred action + // is to use a helper app, we have to pick the specific helper app item). + if (handlerInfo.alwaysAskBeforeHandling) { + menu.selectedItem = askMenuItem; + } else { + // The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify + // the actions the application can take with content of various types. + // But since we've stopped support for plugins, there's no value + // identifying the "use plugin" action, so we use this constant instead. + const kActionUsePlugin = 5; + + switch (handlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.handleInternally: + if (internalMenuItem) { + menu.selectedItem = internalMenuItem; + } else { + console.error("No menu item defined to set!"); + } + break; + case Ci.nsIHandlerInfo.useSystemDefault: + // We might not have a default item if we're not aware of an + // OS-default handler for this type: + menu.selectedItem = defaultMenuItem || askMenuItem; + break; + case Ci.nsIHandlerInfo.useHelperApp: + if (preferredApp) { + let preferredItem = possibleAppMenuItems.find(v => + v.handlerApp.equals(preferredApp) + ); + if (preferredItem) { + menu.selectedItem = preferredItem; + } else { + // This shouldn't happen, but let's make sure we end up with a + // selected item: + let possible = possibleAppMenuItems + .map(v => v.handlerApp && v.handlerApp.name) + .join(", "); + console.error( + new Error( + `Preferred handler for ${handlerInfo.type} not in list of possible handlers!? (List: ${possible})` + ) + ); + menu.selectedItem = askMenuItem; + } + } + break; + case kActionUsePlugin: + // We no longer support plugins, select "ask" instead: + menu.selectedItem = askMenuItem; + break; + case Ci.nsIHandlerInfo.saveToDisk: + menu.selectedItem = saveMenuItem; + break; + } + } + }, + + // Sorting & Filtering + + _sortColumn: null, + + /** + * Sort the list when the user clicks on a column header. + */ + sort(event) { + if (event.button != 0) { + return; + } + var column = event.target; + + // If the user clicked on a new sort column, remove the direction indicator + // from the old column. + if (this._sortColumn && this._sortColumn != column) { + this._sortColumn.removeAttribute("sortDirection"); + } + + this._sortColumn = column; + + // Set (or switch) the sort direction indicator. + if (column.getAttribute("sortDirection") == "ascending") { + column.setAttribute("sortDirection", "descending"); + } else { + column.setAttribute("sortDirection", "ascending"); + } + + this._sortListView(); + }, + + async _sortListView() { + if (!this._sortColumn) { + return; + } + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + await document.l10n.translateFragment(this._list); + let items = Array.from(this._list.children); + + let textForNode; + if (this._sortColumn.getAttribute("value") === "type") { + textForNode = n => n.querySelector(".typeDescription").textContent; + } else { + textForNode = n => n.querySelector(".actionsMenu").getAttribute("label"); + } + + let sortDir = this._sortColumn.getAttribute("sortDirection"); + let multiplier = sortDir == "descending" ? -1 : 1; + items.sort( + (a, b) => multiplier * comp.compare(textForNode(a), textForNode(b)) + ); + + // Re-append items in the correct order: + items.forEach(item => this._list.appendChild(item)); + }, + + _filterView(frag = this._list) { + const filterValue = this._filter.value.toLowerCase(); + for (let elem of frag.children) { + const typeDescription = + elem.querySelector(".typeDescription").textContent; + const actionDescription = elem + .querySelector(".actionDescription") + .getAttribute("value"); + elem.hidden = + !typeDescription.toLowerCase().includes(filterValue) && + !actionDescription.toLowerCase().includes(filterValue); + } + }, + + /** + * Filter the list when the user enters a filter term into the filter field. + */ + filter() { + this._rebuildView(); // FIXME: Should this be await since bug 1508156? + }, + + focusFilterBox() { + this._filter.focus(); + this._filter.select(); + }, + + // Changes + + // Whether or not we are currently storing the action selected by the user. + // We use this to suppress notification-triggered updates to the list when + // we make changes that may spawn such updates. + // XXXgijs: this was definitely necessary when we changed feed preferences + // from within _storeAction and its calltree. Now, it may still be + // necessary, to avoid calling _rebuildView. bug 1499350 has more details. + _storingAction: false, + + onSelectAction(aActionItem) { + this._storingAction = true; + + try { + this._storeAction(aActionItem); + } finally { + this._storingAction = false; + } + }, + + _storeAction(aActionItem) { + var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper; + + let action = parseInt(aActionItem.getAttribute("action")); + + // Set the preferred application handler. + // We leave the existing preferred app in the list when we set + // the preferred action to something other than useHelperApp so that + // legacy datastores that don't have the preferred app in the list + // of possible apps still include the preferred app in the list of apps + // the user can choose to handle the type. + if (action == Ci.nsIHandlerInfo.useHelperApp) { + handlerInfo.preferredApplicationHandler = aActionItem.handlerApp; + } + + // Set the "always ask" flag. + if (action == Ci.nsIHandlerInfo.alwaysAsk) { + handlerInfo.alwaysAskBeforeHandling = true; + } else { + handlerInfo.alwaysAskBeforeHandling = false; + } + + // Set the preferred action. + handlerInfo.preferredAction = action; + + handlerInfo.store(); + + // Update the action label and image to reflect the new preferred action. + this.selectedHandlerListItem.refreshAction(); + }, + + manageApp(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper; + + let onComplete = () => { + // Rebuild the actions menu so that we revert to the previous selection, + // or "Always ask" if the previous default application has been removed + this.rebuildActionsMenu(); + + // update the richlistitem too. Will be visible when selecting another row + this.selectedHandlerListItem.refreshAction(); + }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/applicationManager.xhtml", + { features: "resizable=no", closingCallback: onComplete }, + handlerInfo + ); + }, + + async chooseApp(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var handlerApp; + let chooseAppCallback = aHandlerApp => { + // Rebuild the actions menu whether the user picked an app or canceled. + // If they picked an app, we want to add the app to the menu and select it. + // If they canceled, we want to go back to their previous selection. + this.rebuildActionsMenu(); + + // If the user picked a new app from the menu, select it. + if (aHandlerApp) { + let typeItem = this._list.selectedItem; + let actionsMenu = typeItem.querySelector(".actionsMenu"); + let menuItems = actionsMenu.menupopup.childNodes; + for (let i = 0; i < menuItems.length; i++) { + let menuItem = menuItems[i]; + if (menuItem.handlerApp && menuItem.handlerApp.equals(aHandlerApp)) { + actionsMenu.selectedIndex = i; + this.onSelectAction(menuItem); + break; + } + } + } + }; + + if (AppConstants.platform == "win") { + var params = {}; + var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper; + + params.mimeInfo = handlerInfo.wrappedHandlerInfo; + params.title = await document.l10n.formatValue( + "applications-select-helper" + ); + if ("id" in handlerInfo.description) { + params.description = await document.l10n.formatValue( + handlerInfo.description.id, + handlerInfo.description.args + ); + } else { + params.description = handlerInfo.typeDescription.raw; + } + params.filename = null; + params.handlerApp = null; + + let onAppSelected = () => { + if (this.isValidHandlerApp(params.handlerApp)) { + handlerApp = params.handlerApp; + + // Add the app to the type's list of possible handlers. + handlerInfo.addPossibleApplicationHandler(handlerApp); + } + + chooseAppCallback(handlerApp); + }; + + gSubDialog.open( + "chrome://global/content/appPicker.xhtml", + { closingCallback: onAppSelected }, + params + ); + } else { + let winTitle = await document.l10n.formatValue( + "applications-select-helper" + ); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = aResult => { + if ( + aResult == Ci.nsIFilePicker.returnOK && + fp.file && + this._isValidHandlerExecutable(fp.file) + ) { + handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.name = getFileDisplayName(fp.file); + handlerApp.executable = fp.file; + + // Add the app to the type's list of possible handlers. + let handler = this.selectedHandlerListItem.handlerInfoWrapper; + handler.addPossibleApplicationHandler(handlerApp); + + chooseAppCallback(handlerApp); + } + }; + + // Prompt the user to pick an app. If they pick one, and it's a valid + // selection, then add it to the list of possible handlers. + fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + fp.open(fpCallback); + } + }, + + _getIconURLForHandlerApp(aHandlerApp) { + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) { + return this._getIconURLForFile(aHandlerApp.executable); + } + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) { + return this._getIconURLForWebApp(aHandlerApp.uriTemplate); + } + + // We know nothing about other kinds of handler apps. + return ""; + }, + + _getIconURLForFile(aFile) { + var fph = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + var urlSpec = fph.getURLSpecFromActualFile(aFile); + + return "moz-icon://" + urlSpec + "?size=16"; + }, + + _getIconURLForWebApp(aWebAppURITemplate) { + var uri = Services.io.newURI(aWebAppURITemplate); + + // Unfortunately we can't use the favicon service to get the favicon, + // because the service looks in the annotations table for a record with + // the exact URL we give it, and users won't have such records for URLs + // they don't visit, and users won't visit the web app's URL template, + // they'll only visit URLs derived from that template (i.e. with %s + // in the template replaced by the URL of the content being handled). + + if ( + /^https?$/.test(uri.scheme) && + Services.prefs.getBoolPref("browser.chrome.site_icons") + ) { + return uri.prePath + "/favicon.ico"; + } + + return ""; + }, + + // DOWNLOADS + + /* + * Preferences: + * + * browser.download.useDownloadDir - bool + * True - Save files directly to the folder configured via the + * browser.download.folderList preference. + * False - Always ask the user where to save a file and default to + * browser.download.lastDir when displaying a folder picker dialog. + * browser.download.always_ask_before_handling_new_types - bool + * Defines the default behavior for new file handlers. + * True - When downloading a file that doesn't match any existing + * handlers, ask the user whether to save or open the file. + * False - Save the file. The user can change the default action in + * the Applications section in the preferences UI. + * browser.download.dir - local file handle + * A local folder the user may have selected for downloaded files to be + * saved. Migration of other browser settings may also set this path. + * This folder is enabled when folderList equals 2. + * browser.download.lastDir - local file handle + * May contain the last folder path accessed when the user browsed + * via the file save-as dialog. (see contentAreaUtils.js) + * browser.download.folderList - int + * Indicates the location users wish to save downloaded files too. + * It is also used to display special file labels when the default + * download location is either the Desktop or the Downloads folder. + * Values: + * 0 - The desktop is the default download location. + * 1 - The system's downloads folder is the default download location. + * 2 - The default download location is elsewhere as specified in + * browser.download.dir. + * browser.download.downloadDir + * deprecated. + * browser.download.defaultFolder + * deprecated. + */ + + /** + * Disables the downloads folder field and Browse button if the default + * download directory pref is locked (e.g., by the DownloadDirectory or + * DefaultDownloadDirectory policies) + */ + readUseDownloadDir() { + document.getElementById("downloadFolder").disabled = + document.getElementById("chooseFolder").disabled = + document.getElementById("saveTo").disabled = + Preferences.get("browser.download.dir").locked || + Preferences.get("browser.download.folderList").locked; + // don't override the preference's value in UI + return undefined; + }, + + /** + * Displays a file picker in which the user can choose the location where + * downloads are automatically saved, updating preferences and UI in + * response to the choice, if one is made. + */ + chooseFolder() { + return this.chooseFolderTask().catch(console.error); + }, + async chooseFolderTask() { + let [title] = await document.l10n.formatValues([ + { id: "choose-download-folder-title" }, + ]); + let folderListPref = Preferences.get("browser.download.folderList"); + let currentDirPref = await this._indexToFolder(folderListPref.value); + let defDownloads = await this._indexToFolder(1); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + + fp.init(window, title, Ci.nsIFilePicker.modeGetFolder); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + // First try to open what's currently configured + if (currentDirPref && currentDirPref.exists()) { + fp.displayDirectory = currentDirPref; + } else if (defDownloads && defDownloads.exists()) { + // Try the system's download dir + fp.displayDirectory = defDownloads; + } else { + // Fall back to Desktop + fp.displayDirectory = await this._indexToFolder(0); + } + + let result = await new Promise(resolve => fp.open(resolve)); + if (result != Ci.nsIFilePicker.returnOK) { + return; + } + + let downloadDirPref = Preferences.get("browser.download.dir"); + downloadDirPref.value = fp.file; + folderListPref.value = await this._folderToIndex(fp.file); + // Note, the real prefs will not be updated yet, so dnld manager's + // userDownloadsDirectory may not return the right folder after + // this code executes. displayDownloadDirPref will be called on + // the assignment above to update the UI. + }, + + /** + * Initializes the download folder display settings based on the user's + * preferences. + */ + displayDownloadDirPref() { + this.displayDownloadDirPrefTask().catch(console.error); + + // don't override the preference's value in UI + return undefined; + }, + + async displayDownloadDirPrefTask() { + // We're async for localization reasons, and we can get called several + // times in the same turn of the event loop (!) because of how the + // preferences bindings work... but the speed of localization + // shouldn't impact what gets displayed to the user in the end - the + // last call should always win. + // To accomplish this, store a unique object when we enter this function, + // and if by the end of the function that stored object has been + // overwritten, don't update the UI but leave it to the last + // caller to this function to do. + let token = {}; + this._downloadDisplayToken = token; + + var downloadFolder = document.getElementById("downloadFolder"); + + let folderIndex = Preferences.get("browser.download.folderList").value; + // For legacy users using cloudstorage pref with folderIndex as 3 (See bug 1751093), + // compute folderIndex using the current directory pref + if (folderIndex == 3) { + let currentDirPref = Preferences.get("browser.download.dir"); + folderIndex = currentDirPref.value + ? await this._folderToIndex(currentDirPref.value) + : 1; + } + + // Display a 'pretty' label or the path in the UI. + let { folderDisplayName, file } = + await this._getSystemDownloadFolderDetails(folderIndex); + // Figure out an icon url: + let fph = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + let iconUrlSpec = fph.getURLSpecFromDir(file); + + // Ensure that the last entry to this function always wins + // (see comment at the start of this method): + if (this._downloadDisplayToken != token) { + return; + } + // note: downloadFolder.value is not read elsewhere in the code, its only purpose is to display to the user + downloadFolder.value = folderDisplayName; + downloadFolder.style.backgroundImage = + "url(moz-icon://" + iconUrlSpec + "?size=16)"; + }, + + async _getSystemDownloadFolderDetails(folderIndex) { + let downloadsDir = await this._getDownloadsFolder("Downloads"); + let desktopDir = await this._getDownloadsFolder("Desktop"); + let currentDirPref = Preferences.get("browser.download.dir"); + + let file; + let firefoxLocalizedName; + if (folderIndex == 2 && currentDirPref.value) { + file = currentDirPref.value; + if (file.equals(downloadsDir)) { + folderIndex = 1; + } else if (file.equals(desktopDir)) { + folderIndex = 0; + } + } + switch (folderIndex) { + case 2: // custom path, handled above. + break; + + case 1: { + // downloads + file = downloadsDir; + firefoxLocalizedName = await document.l10n.formatValues([ + { id: "downloads-folder-name" }, + ]); + break; + } + + case 0: + // fall through + default: { + file = desktopDir; + firefoxLocalizedName = await document.l10n.formatValues([ + { id: "desktop-folder-name" }, + ]); + } + } + if (firefoxLocalizedName) { + let folderDisplayName, leafName; + // Either/both of these can throw, so check for failures in both cases + // so we don't just break display of the download pref: + try { + folderDisplayName = file.displayName; + } catch (ex) { + /* ignored */ + } + try { + leafName = file.leafName; + } catch (ex) { + /* ignored */ + } + + // If we found a localized name that's different from the leaf name, + // use that: + if (folderDisplayName && folderDisplayName != leafName) { + return { file, folderDisplayName }; + } + + // Otherwise, check if we've got a localized name ourselves. + if (firefoxLocalizedName) { + // You can't move the system download or desktop dir on macOS, + // so if those are in use just display them. On other platforms + // only do so if the folder matches the localized name. + if ( + AppConstants.platform == "mac" || + leafName == firefoxLocalizedName + ) { + return { file, folderDisplayName: firefoxLocalizedName }; + } + } + } + // If we get here, attempts to use a "pretty" name failed. Just display + // the full path: + if (file) { + // Force the left-to-right direction when displaying a custom path. + return { file, folderDisplayName: `\u2066${file.path}\u2069` }; + } + // Don't even have a file - fall back to desktop directory for the + // use of the icon, and an empty label: + file = desktopDir; + return { file, folderDisplayName: "" }; + }, + + /** + * Returns the Downloads folder. If aFolder is "Desktop", then the Downloads + * folder returned is the desktop folder; otherwise, it is a folder whose name + * indicates that it is a download folder and whose path is as determined by + * the XPCOM directory service via the download manager's attribute + * defaultDownloadsDirectory. + * + * @throws if aFolder is not "Desktop" or "Downloads" + */ + async _getDownloadsFolder(aFolder) { + switch (aFolder) { + case "Desktop": + return Services.dirsvc.get("Desk", Ci.nsIFile); + case "Downloads": + let downloadsDir = await Downloads.getSystemDownloadsDirectory(); + return new FileUtils.File(downloadsDir); + } + throw new Error( + "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'" + ); + }, + + /** + * Determines the type of the given folder. + * + * @param aFolder + * the folder whose type is to be determined + * @returns integer + * 0 if aFolder is the Desktop or is unspecified, + * 1 if aFolder is the Downloads folder, + * 2 otherwise + */ + async _folderToIndex(aFolder) { + if (!aFolder || aFolder.equals(await this._getDownloadsFolder("Desktop"))) { + return 0; + } else if (aFolder.equals(await this._getDownloadsFolder("Downloads"))) { + return 1; + } + return 2; + }, + + /** + * Converts an integer into the corresponding folder. + * + * @param aIndex + * an integer + * @returns the Desktop folder if aIndex == 0, + * the Downloads folder if aIndex == 1, + * the folder stored in browser.download.dir + */ + _indexToFolder(aIndex) { + switch (aIndex) { + case 0: + return this._getDownloadsFolder("Desktop"); + case 1: + return this._getDownloadsFolder("Downloads"); + } + var currentDirPref = Preferences.get("browser.download.dir"); + return currentDirPref.value; + }, +}; + +gMainPane.initialized = new Promise(res => { + gMainPane.setInitialized = res; +}); + +// Utilities + +function getFileDisplayName(file) { + if (AppConstants.platform == "win") { + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } + } + if (AppConstants.platform == "macosx") { + if (file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } + } + return file.leafName; +} + +function getLocalHandlerApp(aFile) { + var localHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandlerApp.name = getFileDisplayName(aFile); + localHandlerApp.executable = aFile; + + return localHandlerApp; +} + +// eslint-disable-next-line no-undef +let gHandlerListItemFragment = MozXULElement.parseXULToFragment(` + <richlistitem> + <hbox class="typeContainer" flex="1" align="center"> + <image class="typeIcon" width="16" height="16" + src="moz-icon://goat?size=16"/> + <label class="typeDescription" flex="1" crop="end"/> + </hbox> + <hbox class="actionContainer" flex="1" align="center"> + <image class="actionIcon" width="16" height="16"/> + <label class="actionDescription" flex="1" crop="end"/> + </hbox> + <hbox class="actionsMenuContainer" flex="1"> + <menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1"> + <menupopup/> + </menulist> + </hbox> + </richlistitem> +`); + +/** + * This is associated to <richlistitem> elements in the handlers view. + */ +class HandlerListItem { + static forNode(node) { + return gNodeToObjectMap.get(node); + } + + constructor(handlerInfoWrapper) { + this.handlerInfoWrapper = handlerInfoWrapper; + } + + setOrRemoveAttributes(iterable) { + for (let [selector, name, value] of iterable) { + let node = selector ? this.node.querySelector(selector) : this.node; + if (value) { + node.setAttribute(name, value); + } else { + node.removeAttribute(name); + } + } + } + + createNode(list) { + list.appendChild(document.importNode(gHandlerListItemFragment, true)); + this.node = list.lastChild; + gNodeToObjectMap.set(this.node, this); + } + + setupNode() { + this.node + .querySelector(".actionsMenu") + .addEventListener("command", event => + gMainPane.onSelectAction(event.originalTarget) + ); + + let typeDescription = this.handlerInfoWrapper.typeDescription; + this.setOrRemoveAttributes([ + [null, "type", this.handlerInfoWrapper.type], + [".typeIcon", "src", this.handlerInfoWrapper.smallIcon], + ]); + localizeElement( + this.node.querySelector(".typeDescription"), + typeDescription + ); + this.showActionsMenu = false; + } + + refreshAction() { + let { actionIconClass } = this.handlerInfoWrapper; + this.setOrRemoveAttributes([ + [null, APP_ICON_ATTR_NAME, actionIconClass], + [ + ".actionIcon", + "src", + actionIconClass ? null : this.handlerInfoWrapper.actionIcon, + ], + ]); + const selectedItem = this.node.querySelector("[selected=true]"); + if (!selectedItem) { + console.error("No selected item for " + this.handlerInfoWrapper.type); + return; + } + const { id, args } = document.l10n.getAttributes(selectedItem); + localizeElement(this.node.querySelector(".actionDescription"), { + id: id + "-label", + args, + }); + localizeElement(this.node.querySelector(".actionsMenu"), { id, args }); + } + + set showActionsMenu(value) { + this.setOrRemoveAttributes([ + [".actionContainer", "hidden", value], + [".actionsMenuContainer", "hidden", !value], + ]); + } +} + +/** + * This API facilitates dual-model of some localization APIs which + * may operate on raw strings of l10n id/args pairs. + * + * The l10n can be: + * + * {raw: string} - raw strings to be used as text value of the element + * {id: string} - l10n-id + * {id: string, args: object} - l10n-id + l10n-args + */ +function localizeElement(node, l10n) { + if (l10n.hasOwnProperty("raw")) { + node.removeAttribute("data-l10n-id"); + node.textContent = l10n.raw; + } else { + document.l10n.setAttributes(node, l10n.id, l10n.args); + } +} + +/** + * This object wraps nsIHandlerInfo with some additional functionality + * the Applications prefpane needs to display and allow modification of + * the list of handled types. + * + * We create an instance of this wrapper for each entry we might display + * in the prefpane, and we compose the instances from various sources, + * including the handler service. + * + * We don't implement all the original nsIHandlerInfo functionality, + * just the stuff that the prefpane needs. + */ +class HandlerInfoWrapper { + constructor(type, handlerInfo) { + this.type = type; + this.wrappedHandlerInfo = handlerInfo; + this.disambiguateDescription = false; + } + + get description() { + if (this.wrappedHandlerInfo.description) { + return { raw: this.wrappedHandlerInfo.description }; + } + + if (this.primaryExtension) { + var extension = this.primaryExtension.toUpperCase(); + return { id: "applications-file-ending", args: { extension } }; + } + + return { raw: this.type }; + } + + /** + * Describe, in a human-readable fashion, the type represented by the given + * handler info object. Normally this is just the description, but if more + * than one object presents the same description, "disambiguateDescription" + * is set and we annotate the duplicate descriptions with the type itself + * to help users distinguish between those types. + */ + get typeDescription() { + if (this.disambiguateDescription) { + const description = this.description; + if (description.id) { + // Pass through the arguments: + let { args = {} } = description; + args.type = this.type; + return { + id: description.id + "-with-type", + args, + }; + } + + return { + id: "applications-type-description-with-type", + args: { + "type-description": description.raw, + type: this.type, + }, + }; + } + + return this.description; + } + + get actionIconClass() { + if (this.alwaysAskBeforeHandling) { + return "ask"; + } + + switch (this.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + return "save"; + + case Ci.nsIHandlerInfo.handleInternally: + if (this instanceof InternalHandlerInfoWrapper) { + return "handleInternally"; + } + break; + } + + return ""; + } + + get actionIcon() { + switch (this.preferredAction) { + case Ci.nsIHandlerInfo.useSystemDefault: + return this.iconURLForSystemDefault; + + case Ci.nsIHandlerInfo.useHelperApp: + let preferredApp = this.preferredApplicationHandler; + if (gMainPane.isValidHandlerApp(preferredApp)) { + return gMainPane._getIconURLForHandlerApp(preferredApp); + } + + // This should never happen, but if preferredAction is set to some weird + // value, then fall back to the generic application icon. + // Explicit fall-through + default: + return ICON_URL_APP; + } + } + + get iconURLForSystemDefault() { + // Handler info objects for MIME types on some OSes implement a property bag + // interface from which we can get an icon for the default app, so if we're + // dealing with a MIME type on one of those OSes, then try to get the icon. + if ( + this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo instanceof Ci.nsIPropertyBag + ) { + try { + let url = this.wrappedHandlerInfo.getProperty( + "defaultApplicationIconURL" + ); + if (url) { + return url + "?size=16"; + } + } catch (ex) {} + } + + // If this isn't a MIME type object on an OS that supports retrieving + // the icon, or if we couldn't retrieve the icon for some other reason, + // then use a generic icon. + return ICON_URL_APP; + } + + get preferredApplicationHandler() { + return this.wrappedHandlerInfo.preferredApplicationHandler; + } + + set preferredApplicationHandler(aNewValue) { + this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue; + + // Make sure the preferred handler is in the set of possible handlers. + if (aNewValue) { + this.addPossibleApplicationHandler(aNewValue); + } + } + + get possibleApplicationHandlers() { + return this.wrappedHandlerInfo.possibleApplicationHandlers; + } + + addPossibleApplicationHandler(aNewHandler) { + for (let app of this.possibleApplicationHandlers.enumerate()) { + if (app.equals(aNewHandler)) { + return; + } + } + this.possibleApplicationHandlers.appendElement(aNewHandler); + } + + removePossibleApplicationHandler(aHandler) { + var defaultApp = this.preferredApplicationHandler; + if (defaultApp && aHandler.equals(defaultApp)) { + // If the app we remove was the default app, we must make sure + // it won't be used anymore + this.alwaysAskBeforeHandling = true; + this.preferredApplicationHandler = null; + } + + var handlers = this.possibleApplicationHandlers; + for (var i = 0; i < handlers.length; ++i) { + var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if (handler.equals(aHandler)) { + handlers.removeElementAt(i); + break; + } + } + } + + get hasDefaultHandler() { + return this.wrappedHandlerInfo.hasDefaultHandler; + } + + get defaultDescription() { + return this.wrappedHandlerInfo.defaultDescription; + } + + // What to do with content of this type. + get preferredAction() { + // If the action is to use a helper app, but we don't have a preferred + // handler app, then switch to using the system default, if any; otherwise + // fall back to saving to disk, which is the default action in nsMIMEInfo. + // Note: "save to disk" is an invalid value for protocol info objects, + // but the alwaysAskBeforeHandling getter will detect that situation + // and always return true in that case to override this invalid value. + if ( + this.wrappedHandlerInfo.preferredAction == + Ci.nsIHandlerInfo.useHelperApp && + !gMainPane.isValidHandlerApp(this.preferredApplicationHandler) + ) { + if (this.wrappedHandlerInfo.hasDefaultHandler) { + return Ci.nsIHandlerInfo.useSystemDefault; + } + return Ci.nsIHandlerInfo.saveToDisk; + } + + return this.wrappedHandlerInfo.preferredAction; + } + + set preferredAction(aNewValue) { + this.wrappedHandlerInfo.preferredAction = aNewValue; + } + + get alwaysAskBeforeHandling() { + // If this is a protocol type and the preferred action is "save to disk", + // which is invalid for such types, then return true here to override that + // action. This could happen when the preferred action is to use a helper + // app, but the preferredApplicationHandler is invalid, and there isn't + // a default handler, so the preferredAction getter returns save to disk + // instead. + if ( + !(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + this.preferredAction == Ci.nsIHandlerInfo.saveToDisk + ) { + return true; + } + + return this.wrappedHandlerInfo.alwaysAskBeforeHandling; + } + + set alwaysAskBeforeHandling(aNewValue) { + this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue; + } + + // The primary file extension associated with this type, if any. + get primaryExtension() { + try { + if ( + this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo.primaryExtension + ) { + return this.wrappedHandlerInfo.primaryExtension; + } + } catch (ex) {} + + return null; + } + + store() { + gHandlerService.store(this.wrappedHandlerInfo); + } + + get smallIcon() { + return this._getIcon(16); + } + + _getIcon(aSize) { + if (this.primaryExtension) { + return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize; + } + + if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) { + return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type; + } + + // FIXME: consider returning some generic icon when we can't get a URL for + // one (for example in the case of protocol schemes). Filed as bug 395141. + return null; + } +} + +/** + * InternalHandlerInfoWrapper provides a basic mechanism to create an internal + * mime type handler that can be enabled/disabled in the applications preference + * menu. + */ +class InternalHandlerInfoWrapper extends HandlerInfoWrapper { + constructor(mimeType, extension) { + let type = gMIMEService.getFromTypeAndExtension(mimeType, extension); + super(mimeType || type.type, type); + } + + // Override store so we so we can notify any code listening for registration + // or unregistration of this handler. + store() { + super.store(); + } + + get preventInternalViewing() { + return false; + } + + get enabled() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } +} + +class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper { + constructor() { + super(TYPE_PDF, null); + } + + get preventInternalViewing() { + return Services.prefs.getBoolPref(PREF_PDFJS_DISABLED); + } + + // PDF is always shown in the list, but the 'show internally' option is + // hidden when the internal PDF viewer is disabled. + get enabled() { + return true; + } +} + +class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper { + get enabled() { + return DownloadIntegration.shouldViewDownloadInternally(this.type); + } +} + +const AppearanceChooser = { + // NOTE: This order must match the values of the + // layout.css.prefers-color-scheme.content-override + // preference. + choices: ["dark", "light", "auto"], + chooser: null, + radios: null, + warning: null, + + init() { + this.chooser = document.getElementById("web-appearance-chooser"); + this.radios = [...this.chooser.querySelectorAll("input")]; + for (let radio of this.radios) { + radio.addEventListener("change", e => { + let index = this.choices.indexOf(e.target.value); + // The pref change callback will update state if needed. + if (index >= 0) { + Services.prefs.setIntPref(PREF_CONTENT_APPEARANCE, index); + } else { + // Shouldn't happen but let's do something sane... + Services.prefs.clearUserPref(PREF_CONTENT_APPEARANCE); + } + }); + } + + // Forward the click to the "colors" button. + document + .getElementById("web-appearance-manage-colors-link") + .addEventListener("click", function (e) { + document.getElementById("colors").click(); + e.preventDefault(); + }); + + document + .getElementById("web-appearance-manage-themes-link") + .addEventListener("click", function (e) { + window.browsingContext.topChromeWindow.BrowserOpenAddonsMgr( + "addons://list/theme" + ); + e.preventDefault(); + }); + + this.warning = document.getElementById("web-appearance-override-warning"); + + FORCED_COLORS_QUERY.addEventListener("change", this); + Services.prefs.addObserver(PREF_USE_SYSTEM_COLORS, this); + Services.obs.addObserver(this, "look-and-feel-changed"); + this._update(); + }, + + _update() { + this._updateWarning(); + this._updateOptions(); + }, + + handleEvent(e) { + this._update(); + }, + + observe(subject, topic, data) { + this._update(); + }, + + destroy() { + Services.prefs.removeObserver(PREF_USE_SYSTEM_COLORS, this); + Services.obs.removeObserver(this, "look-and-feel-changed"); + FORCED_COLORS_QUERY.removeEventListener("change", this); + }, + + _isValueDark(value) { + switch (value) { + case "light": + return false; + case "dark": + return true; + case "auto": + return Services.appinfo.contentThemeDerivedColorSchemeIsDark; + } + throw new Error("Unknown value"); + }, + + _updateOptions() { + let index = Services.prefs.getIntPref(PREF_CONTENT_APPEARANCE); + if (index < 0 || index >= this.choices.length) { + index = Services.prefs + .getDefaultBranch(null) + .getIntPref(PREF_CONTENT_APPEARANCE); + } + let value = this.choices[index]; + for (let radio of this.radios) { + let checked = radio.value == value; + let isDark = this._isValueDark(radio.value); + + radio.checked = checked; + radio.closest("label").classList.toggle("dark", isDark); + } + }, + + _updateWarning() { + let forcingColorsAndNoColorSchemeSupport = + FORCED_COLORS_QUERY.matches && + (AppConstants.platform == "win" || + !Services.prefs.getBoolPref(PREF_USE_SYSTEM_COLORS)); + this.warning.hidden = !forcingColorsAndNoColorSchemeSupport; + }, +}; diff --git a/browser/components/preferences/more-from-mozilla-qr-code-simple-cn.svg b/browser/components/preferences/more-from-mozilla-qr-code-simple-cn.svg new file mode 100644 index 0000000000..5052db9702 --- /dev/null +++ b/browser/components/preferences/more-from-mozilla-qr-code-simple-cn.svg @@ -0,0 +1,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/. --> +<svg width="80" height="80" fill="context-fill" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 2h72a2 2 0 0 1 2 2v72a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2ZM0 4a4 4 0 0 1 4-4h72a4 4 0 0 1 4 4v72a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4Zm7.08 3.08h15.892v15.892H7.08V7.08Zm4.54 2.27H9.35V20.701H20.701V9.35h-9.08Zm18.163-2.27h4.541v2.27h2.27v2.27h-2.27v2.27h-2.27v2.271h4.54v-4.54h2.27V9.35h2.271v6.811h-2.27v2.27h2.27v4.541h-2.27v-2.27h-2.27v-2.27h-2.27v2.27h-2.271v-2.27h-2.27V7.08Zm0 13.622v-2.27h-2.27V9.35h-2.27v13.622h2.27v2.27h-9.081v2.271h-2.27v2.27H13.89v-4.54h-2.27v2.27H7.08v2.27h2.27v2.27h2.27v2.271h9.081v2.27h2.271v2.27h-2.27v2.271h-2.27v-2.27h-2.27v-2.27H7.08v2.27h4.54v2.27H9.35v2.27h6.811v-2.27h2.27v4.54h6.811v-2.27h2.271v2.27h2.27v-4.54h-4.54v-2.27h2.27v-4.54h2.27v-2.271h-4.54v-2.27h2.27v-2.271h2.27v-4.54h2.27v2.27h4.541v2.27h-2.27v2.27h2.27v-2.27h2.27v-4.54h-2.27v-2.271h-2.27v2.27h-2.27v-2.27h-2.27Zm0 0h-2.27v2.27h2.27v-2.27Zm-9.081 11.352v2.27h2.27v2.27h2.27v-4.54h-4.54Zm4.54 9.081h-4.54v2.27h4.54v-2.27Zm-4.54-9.081v-2.27h2.27v-2.271h-4.54v4.54h2.27Zm-9.082 0v-2.27h2.27v2.27h-2.27ZM41.135 9.35V7.08h2.27v2.27h-2.27Zm2.27 20.433h-2.27v-4.54h2.27v2.27h2.27v-2.27h2.271v-4.541h2.27v2.27h2.271v-4.54h2.27V25.242h2.27v2.271h-4.54v-2.27h-2.27v4.54h-6.811Zm9.082 2.27v-2.27h-2.27v2.27h2.27Zm2.27 0h-2.27v4.541h-2.27v4.541h2.27v4.54h-2.27v4.541h-4.541v2.271h-2.27v-2.27h-2.27v2.27h2.27v2.27h-4.541v-4.54h-6.811v2.27h-2.27v-4.54h-4.541v2.27h-2.27v-2.27h-2.271v2.27h2.27v2.27h-4.54v-4.54H13.89v2.27h-2.27v-4.541H9.35v2.27H7.08v2.27h4.54v2.271H7.08v2.27h4.54v-2.27h2.27v2.27h2.271v-2.27h2.27v2.27h6.811v6.811h2.271v2.27h2.27v2.271h-4.54v2.27h2.27v2.271h2.27v-4.54h2.27v-2.271h-2.27v-4.54h4.541v2.27h2.27v2.27h-2.27v2.27h2.27v4.541h-2.27v2.27h2.27v-2.27h2.27v2.27h6.811v-2.27h4.541v-4.54h4.541v2.27h-2.27v4.54h2.27v-2.27h2.27v2.27h9.082V66.11h2.27v2.27h4.541v-2.27h-2.27v-6.811h2.27V45.675h-2.27v13.622h-2.27v-2.27h-2.27v-4.541h-6.811v-2.27h-4.541v-6.811h2.27v4.54h4.54v-4.54h-2.27v-2.27h-6.811v-2.271h2.27v-2.27h2.27v2.27h2.271v-4.54h4.54v9.08h2.271v-2.27h2.27v-2.27h-2.27v-6.811h2.27v-2.27h2.271v2.27h2.27v-4.541h-2.27v-2.27H59.299v2.27h-2.271v2.27h-2.27v2.27Zm2.27 0v2.271h-2.27v-2.27h2.27Zm2.271-2.27h2.27v2.27h2.27v-4.54h-4.54v2.27Zm0 0v2.27h-2.27v-2.27h2.27Zm9.082 29.515h-2.27v2.27h-2.271v2.27h4.54v-4.54ZM54.757 66.11h2.27v2.27h2.271v2.271h4.54v-2.27h-2.27v-2.27h-2.27v-2.271h-4.54v2.27Zm-6.81-2.27v2.27h2.27v-2.27h-2.27Zm-2.271 0v-2.27h-6.811v-2.27h2.27v-2.271h2.27v-2.27h9.082v4.54h-4.54v-2.27h-2.271v2.27h2.27v4.54h-2.27Zm0 0h-9.081v2.27h2.27v2.27h2.27v2.271h2.27v-2.27h2.27v-4.541Zm-9.082-6.811v2.27h-2.27v-2.27h2.27Zm0-2.27h2.27v2.27h-2.27v-2.27Zm0 0h-2.27v-2.271h2.27v2.27Zm-6.81 4.54v-6.811h-2.271v6.811h2.27ZM47.945 7.08h4.541v4.54h-2.27V9.35h-2.27V7.08Zm0 2.27v2.27h2.27v2.27h4.541v2.271h-4.54v2.27h-6.811v-2.27h2.27v-2.27h-2.27v-2.27h2.27V9.35h2.27Zm9.082-2.27H72.92v15.892H57.028V7.08Zm4.54 2.27h-2.27V20.701H70.65V9.35h-9.081ZM11.62 11.62h6.811v6.811H11.621V11.621Zm49.949 0H68.379v6.811H61.57V11.621Zm-18.163 9.082h2.27v2.27h-2.27v-2.27ZM68.38 34.324h2.27v2.27h-2.27v-2.27Zm2.27 2.27h2.27v4.541h-2.27v-4.54Zm-4.54 11.352h2.27v2.27h-2.27v-2.27Zm-9.082 6.811h-2.27v6.811H61.567V54.758h-4.54ZM7.08 57.027h15.892V72.92H7.08V57.028Zm4.54 2.271H9.35V70.65H20.701V59.299h-9.08Zm45.408-2.27h2.27v2.27h-2.27v-2.27Zm-45.408 4.54h6.811v6.811H11.621V61.57Zm59.03 9.082h2.27v2.27h-2.27v-2.27Z"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99l.002.01c-.742-1.847-1.998-2.593-3.025-4.215-.052-.082-.104-.164-.154-.25a2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.024.024 0 0 0-.013 0l-.003.002-.005.003.003-.005c-1.647.964-2.206 2.749-2.256 3.642a3.28 3.28 0 0 0-1.805.696 1.953 1.953 0 0 0-.17-.129 3.04 3.04 0 0 1-.018-1.602 4.856 4.856 0 0 0-1.578 1.22h-.003c-.26-.33-.242-1.416-.227-1.643-.077.031-.15.07-.219.117-.229.163-.444.347-.64.549-.225.227-.43.473-.613.735a5.537 5.537 0 0 0-.88 1.986l-.009.043c-.012.057-.056.346-.064.41l-.002.014c-.057.298-.093.6-.106.903v.034a6.556 6.556 0 0 0 13.017 1.109l.03-.254a6.742 6.742 0 0 0-.426-3.293Zm-7.556 5.132c.03.015.059.03.09.044l.005.003a3.257 3.257 0 0 1-.095-.047Zm6.906-4.79v-.006.007Z" fill="url(#a)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99v.006l.002.007a5.937 5.937 0 0 1-.204 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093-.064.298-.099.6-.106.905v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.254a6.743 6.743 0 0 0-.426-3.293Z" fill="url(#b)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99v.006l.002.007a5.937 5.937 0 0 1-.204 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093-.064.298-.099.6-.106.905v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.254a6.743 6.743 0 0 0-.426-3.293Z" fill="url(#c)"/><path d="m42.879 38.322.04.03a3.57 3.57 0 0 0-.608-.795c-2.039-2.038-.535-4.418-.28-4.54l.002-.004c-1.647.965-2.206 2.75-2.257 3.642.077-.005.152-.011.23-.011a3.307 3.307 0 0 1 2.873 1.677Z" fill="url(#d)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.865 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.073.036.11.053.065.029.129.055.193.079.274.097.562.152.853.164 3.268.154 3.9-3.907 1.542-5.086a2.263 2.263 0 0 1 1.581.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#e)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.865 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.073.036.11.053.065.029.129.055.193.079.274.097.562.152.853.164 3.268.154 3.9-3.907 1.542-5.086a2.263 2.263 0 0 1 1.581.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#f)"/><path d="m37.665 37.134.136.09a3.04 3.04 0 0 1-.019-1.602 4.854 4.854 0 0 0-1.578 1.22c.032-.002.983-.019 1.461.292Z" fill="url(#g)"/><path d="M33.503 40.145c.506 2.989 3.216 5.272 6.291 5.359 2.847.08 4.665-1.572 5.416-3.184a5.937 5.937 0 0 0 .204-4.425v-.012l.002.01c.232 1.52-.54 2.99-1.748 3.986l-.004.008c-2.352 1.917-4.604 1.156-5.059.846a3.524 3.524 0 0 1-.095-.047c-1.372-.656-1.939-1.905-1.817-2.977a1.683 1.683 0 0 1-1.553-.977 2.474 2.474 0 0 1 2.41-.097c.777.352 1.66.387 2.462.097-.003-.054-1.144-.508-1.589-.946-.238-.234-.35-.347-.45-.432a1.987 1.987 0 0 0-.17-.128 9.602 9.602 0 0 0-.135-.09c-.478-.31-1.429-.294-1.46-.293h-.003c-.26-.33-.242-1.415-.227-1.642-.077.03-.15.07-.219.116-.23.164-.444.348-.64.55a5.712 5.712 0 0 0-.616.733 5.536 5.536 0 0 0-.88 1.986c-.003.013-.236 1.031-.121 1.56l.001-.001Z" fill="url(#h)"/><path d="M42.31 37.557c.24.235.445.503.61.795.035.027.07.054.098.08 1.486 1.37.707 3.307.65 3.444 1.207-.994 1.978-2.466 1.747-3.985-.742-1.849-2-2.595-3.025-4.217a7.809 7.809 0 0 1-.154-.25 2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.024.024 0 0 0-.013 0l-.003.002-.004.003c-.254.12-1.758 2.501.28 4.538v.003Z" fill="url(#i)"/><path d="M43.017 38.433a1.349 1.349 0 0 0-.098-.08l-.04-.03a2.263 2.263 0 0 0-1.581-.384c2.358 1.179 1.725 5.239-1.543 5.086a2.915 2.915 0 0 1-.853-.164 3.434 3.434 0 0 1-.303-.132l.005.003c.455.311 2.706 1.071 5.059-.846l.004-.008c.058-.137.837-2.074-.65-3.444Z" fill="url(#j)"/><path d="M37.055 40.581s.302-1.127 2.167-1.127c.201 0 .778-.563.788-.726a3.265 3.265 0 0 1-2.461-.097 2.471 2.471 0 0 0-2.41.097 1.683 1.683 0 0 0 1.552.977c-.121 1.072.446 2.32 1.817 2.977l.091.045c-.8-.414-1.462-1.196-1.544-2.145Z" fill="url(#k)"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99l.002.01c-.742-1.847-1.998-2.593-3.025-4.215a8.036 8.036 0 0 1-.154-.25 2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.024.024 0 0 0-.013 0l-.003.002-.005.003.003-.005c-1.647.964-2.206 2.749-2.256 3.642.076-.005.152-.011.23-.011a3.308 3.308 0 0 1 2.872 1.677 2.263 2.263 0 0 0-1.58-.384c2.357 1.18 1.725 5.239-1.543 5.087a2.916 2.916 0 0 1-.853-.165 3.432 3.432 0 0 1-.303-.132l.004.003a3.524 3.524 0 0 1-.095-.047l.09.044c-.8-.413-1.461-1.195-1.544-2.144 0 0 .303-1.128 2.167-1.128.202 0 .778-.563.789-.726-.003-.053-1.144-.507-1.589-.945-.237-.234-.35-.347-.45-.432a1.987 1.987 0 0 0-.17-.128 3.04 3.04 0 0 1-.018-1.603 4.854 4.854 0 0 0-1.578 1.22h-.003c-.26-.33-.241-1.415-.226-1.642-.077.03-.15.07-.22.116-.229.164-.443.347-.64.549-.224.227-.43.473-.613.735v.001-.001a5.536 5.536 0 0 0-.88 1.986l-.008.043c-.013.058-.068.35-.076.414-.05.302-.083.607-.097.914v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.253a6.743 6.743 0 0 0-.426-3.294Zm-.65.337v.006-.007Z" fill="url(#l)"/><defs><radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(44.777 34.536) scale(13.6677)"><stop offset=".129" stop-color="#FFBD4F"/><stop offset=".186" stop-color="#FFAC31"/><stop offset=".247" stop-color="#FF9D17"/><stop offset=".283" stop-color="#FF980E"/><stop offset=".403" stop-color="#FF563B"/><stop offset=".467" stop-color="#FF3750"/><stop offset=".71" stop-color="#F5156C"/><stop offset=".782" stop-color="#EB0878"/><stop offset=".86" stop-color="#E50080"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.702 40.118) scale(13.6677)"><stop offset=".3" stop-color="#960E18"/><stop offset=".351" stop-color="#B11927" stop-opacity=".74"/><stop offset=".435" stop-color="#DB293D" stop-opacity=".343"/><stop offset=".497" stop-color="#F5334B" stop-opacity=".094"/><stop offset=".53" stop-color="#FF3750" stop-opacity="0"/></radialGradient><radialGradient id="d" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(41.394 31.491) scale(9.90066)"><stop offset=".132" stop-color="#FFF44F"/><stop offset=".252" stop-color="#FFDC3E"/><stop offset=".506" stop-color="#FF9D12"/><stop offset=".526" stop-color="#FF980E"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(38.18 43.67) scale(6.5074)"><stop offset=".353" stop-color="#3A8EE6"/><stop offset=".472" stop-color="#5C79F0"/><stop offset=".669" stop-color="#9059FF"/><stop offset="1" stop-color="#C139E6"/></radialGradient><radialGradient id="f" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.35414 -.81093 .9494 3.92687 40.363 38.945)"><stop offset=".206" stop-color="#9059FF" stop-opacity="0"/><stop offset=".278" stop-color="#8C4FF3" stop-opacity=".064"/><stop offset=".747" stop-color="#7716A8" stop-opacity=".45"/><stop offset=".975" stop-color="#6E008B" stop-opacity=".6"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 34.028) scale(4.68222)"><stop stop-color="#FFE226"/><stop offset=".121" stop-color="#FFDB27"/><stop offset=".295" stop-color="#FFC82A"/><stop offset=".502" stop-color="#FFA930"/><stop offset=".732" stop-color="#FF7E37"/><stop offset=".792" stop-color="#FF7139"/></radialGradient><radialGradient id="h" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.254 30.983) scale(19.9772)"><stop offset=".113" stop-color="#FFF44F"/><stop offset=".456" stop-color="#FF980E"/><stop offset=".622" stop-color="#FF5634"/><stop offset=".716" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="i" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(83.976 2.969 39.153) scale(14.6396 9.60783)"><stop stop-color="#FFF44F"/><stop offset=".06" stop-color="#FFE847"/><stop offset=".168" stop-color="#FFC830"/><stop offset=".304" stop-color="#FF980E"/><stop offset=".356" stop-color="#FF8B16"/><stop offset=".455" stop-color="#FF672A"/><stop offset=".57" stop-color="#FF3647"/><stop offset=".737" stop-color="#E31587"/></radialGradient><radialGradient id="j" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 35.72) scale(12.4701)"><stop offset=".137" stop-color="#FFF44F"/><stop offset=".48" stop-color="#FF980E"/><stop offset=".592" stop-color="#FF5634"/><stop offset=".655" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="k" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(42.747 36.397) scale(13.6491)"><stop offset=".094" stop-color="#FFF44F"/><stop offset=".231" stop-color="#FFE141"/><stop offset=".509" stop-color="#FFAF1E"/><stop offset=".626" stop-color="#FF980E"/></radialGradient><linearGradient id="a" x1="45.198" y1="35.108" x2="34.314" y2="45.609" gradientUnits="userSpaceOnUse"><stop offset=".048" stop-color="#FFF44F"/><stop offset=".111" stop-color="#FFE847"/><stop offset=".225" stop-color="#FFC830"/><stop offset=".368" stop-color="#FF980E"/><stop offset=".401" stop-color="#FF8B16"/><stop offset=".462" stop-color="#FF672A"/><stop offset=".534" stop-color="#FF3647"/><stop offset=".705" stop-color="#E31587"/></linearGradient><linearGradient id="l" x1="45.066" y1="35.053" x2="35.806" y2="44.314" gradientUnits="userSpaceOnUse"><stop offset=".167" stop-color="#FFF44F" stop-opacity=".8"/><stop offset=".266" stop-color="#FFF44F" stop-opacity=".634"/><stop offset=".489" stop-color="#FFF44F" stop-opacity=".217"/><stop offset=".6" stop-color="#FFF44F" stop-opacity="0"/></linearGradient></defs></svg> diff --git a/browser/components/preferences/more-from-mozilla-qr-code-simple.svg b/browser/components/preferences/more-from-mozilla-qr-code-simple.svg new file mode 100644 index 0000000000..279595db10 --- /dev/null +++ b/browser/components/preferences/more-from-mozilla-qr-code-simple.svg @@ -0,0 +1,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/. --> +<svg width="80" height="80" fill="context-fill" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M76 2H4a2 2 0 0 0-2 2v72a2 2 0 0 0 2 2h72a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2ZM4 0a4 4 0 0 0-4 4v72a4 4 0 0 0 4 4h72a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4H4Zm3.08 7.08h15.892v15.892H7.08V7.08Zm4.54 2.27H9.35V20.701H20.701V9.35h-9.08Zm20.434-2.27h2.27v2.27h2.27V7.08h4.541v2.27h2.27v2.27H36.596v2.27h-4.541v2.271h-2.27v-2.27h-2.271v-2.27h2.27V9.35h2.27V7.08Zm-4.541 4.54h-2.27V9.35h2.27v2.27Zm2.27 11.352v-4.54h2.27v-2.27h4.541V13.89h4.541v2.27h4.54v2.27h-2.27v2.27h-2.27v-2.27h-2.27v2.27h-2.27v-2.27h-2.27v2.27h-2.271v2.271h-2.27Zm-4.54 2.27V18.433h2.27v4.54h2.27v2.27h-4.54Zm0 0v2.271h-9.081v-2.27h9.08Zm11.351-2.27h-2.27v-2.27h2.27v2.27Zm2.27 0h-2.27v4.541h-2.27v-2.27h-2.27v2.27h2.27v2.27h4.54V22.973Zm2.271 0v-2.27h-2.27v2.27h2.27Zm2.27 0h-2.27v6.811h4.54v-2.27h-2.27v-4.54Zm2.27 0h-2.27v-2.27h2.27v2.27Zm4.542-6.81h-2.27V22.971h-2.271v2.27h2.27v2.271h2.27v2.27h-2.27v2.27h2.27v-2.27h2.271v2.27h2.27v2.271h2.27v-4.54h2.271v2.27h2.27v2.27h-2.27v2.27h-2.27v2.27h-4.541v6.811h2.27v2.271h4.541v4.541h-6.811v-2.27h-2.27v2.27h-2.27v-2.27h-4.541v2.27h-4.541v-2.27h-4.54v2.27h2.27v2.27h2.27v2.27H45.675v4.541h2.271v2.27h-2.27v2.271h-2.27v-4.54h-4.541v-2.27h-2.27v-2.271h-2.27v2.27h2.27v2.27h-2.27v2.27h-2.271v-2.27h-2.27v-2.27h-2.271v-4.54h2.27v2.27h2.27v-2.27h-2.27v-2.271h2.27v-2.27h-4.54v-2.27h2.27v-2.271h-2.27v-4.54h2.27v-2.271h-2.27v-6.811h2.27v-2.27h-2.27v2.27h-4.54v-2.27h-2.271v2.27h2.27v2.27h-4.54v-4.54h-2.27V36.593H13.89v-4.54h-2.27v-2.27h2.27v-2.271H7.08v6.811h2.27v4.54h4.54v2.271h-2.27v2.27H9.35v-2.27H7.08v2.27h2.27v2.27H7.08v2.271h2.27v-2.27h2.27v2.27h2.27v-4.54h4.541v-2.27h-2.27v-4.541h2.27v2.27h2.27v2.27h2.271v2.27h-2.27v2.27h-2.27v2.271h-2.27v2.27H11.62v2.271H7.08v2.27h4.54v-2.27h4.541v2.27h9.081v9.081h4.541v4.541h-2.27v4.541h4.54v-2.27h2.271v2.27h2.27v-4.54h-4.54v-2.27h2.27v-2.271h2.27v-2.27h2.27v2.27h2.271v2.27h-2.27v6.811H45.675v-4.54h2.271v4.54h2.27v-4.54h2.271v4.54h6.811v-2.27h2.27v2.27h2.27v-2.27h-2.27v-2.27h-4.54v-2.27h-9.081v-2.271H59.297v2.27h2.27v-2.27h2.27v4.54h4.541v4.541h2.271v-2.27h2.27v-4.54h-2.27v-2.271h2.27v-2.27h-2.27v-2.27h2.27v-4.541h-2.27v-4.541h2.27V27.513h-2.27v-2.27h-2.27v2.27h-6.811v-2.27h-4.541v2.27h-2.27v-2.27h-2.271v-2.27h2.27v-4.541h-4.54v-2.27Zm2.27-6.812h2.27v4.54h-2.27v2.271h-2.27v-4.54h2.27V9.35Zm0 0h-6.811V7.08h6.811v2.27Zm0 13.622v-2.27h-2.27v2.27h2.27Zm9.081 6.811v2.27h2.27v2.271h-2.27v2.27h-2.27v4.541h-4.54v2.27h2.27v2.27h4.54v-2.27h2.27v4.541h-2.27v4.541h2.27v6.811h2.271v2.27h-2.27v2.27h4.54v-4.54h2.271v-4.54h-2.27v-4.541h2.27v-4.541h-2.27v-4.54h2.27v-6.811h-2.27v-2.271h2.27v-2.27h-4.54v2.27h-2.271v-2.27h-2.27Zm6.812 24.974h-2.27v2.27h2.27v-2.27Zm0-4.54h-4.541v-2.27h4.54v2.27Zm-4.541-9.082h2.27v2.27h-2.27v-2.27Zm-2.27 0v-4.54h2.27v4.54h-2.27Zm4.54-4.54v-2.27h-2.27v2.27h2.27Zm0 0h2.27v2.27h-2.27v-2.27Zm-4.54 4.54v2.27h-2.27v-2.27h2.27Zm0-11.352v-2.27h-2.27v2.27h2.27ZM57.028 68.38h-2.27v2.27h2.27v-2.27Zm-24.974 0h-2.27v2.27h2.27v-2.27ZM20.702 47.946v2.27h2.27v2.271h-4.54v-4.54h2.27Zm2.27-2.27v2.27h-2.27v-2.27h2.27Zm2.27 0v2.27h2.271v-2.27h-2.27Zm0 0h-2.27v-2.27h2.27v2.27Zm-2.27-9.082v2.27h-2.27v-2.27h2.27Zm0 0h2.27v-2.27h-2.27v2.27ZM9.35 34.324v-2.27h2.27v2.27H9.35Zm15.893 20.433h2.27v-2.27h-2.27v2.27ZM41.135 66.11v4.541h2.27v-4.54h-2.27Zm6.811-4.54v-2.27h2.27v2.27h-2.27Zm-2.27-6.812h-2.27v-2.27h2.27v2.27Zm0 0v2.27h2.27v-2.27h-2.27Zm4.54-20.433v2.27h2.271v-2.27h-2.27Zm2.271-9.081h-2.27v2.27h2.27v-2.27Zm4.54-18.163H72.92v15.892H57.028V7.08Zm4.541 2.27h-2.27V20.701H70.65V9.35h-9.081ZM11.62 11.62h6.811v6.811H11.621V11.621Zm34.056 0h2.27v2.27h-2.27v-2.27Zm15.892 0h6.811v6.811H61.57V11.621Zm-4.54 43.137h-2.27v6.811H61.567V54.758h-4.54ZM7.08 57.027h15.892V72.92H7.08V57.028Zm4.54 2.271H9.35V70.65H20.701V59.299h-9.08Zm45.408-2.27h2.27v2.27h-2.27v-2.27Zm-45.408 4.54h6.811v6.811H11.621V61.57Z"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.315-1.66a6.81 6.81 0 0 1 .663 1.99l.002.01c-.741-1.847-1.998-2.593-3.025-4.215a8.23 8.23 0 0 1-.154-.25 2.078 2.078 0 0 1-.072-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.011-.006.024.024 0 0 0-.013 0l-.003.002-.004.003.002-.005c-1.647.964-2.206 2.749-2.256 3.642a3.28 3.28 0 0 0-1.805.696 1.967 1.967 0 0 0-.17-.129 3.04 3.04 0 0 1-.018-1.602 4.855 4.855 0 0 0-1.578 1.22h-.003c-.26-.33-.242-1.416-.227-1.643-.076.031-.15.07-.218.117-.23.163-.444.347-.641.549a5.73 5.73 0 0 0-.613.735 5.535 5.535 0 0 0-.88 1.986l-.008.043c-.013.057-.057.346-.065.41l-.002.014c-.057.298-.092.6-.106.903v.034a6.556 6.556 0 0 0 13.017 1.109l.03-.254a6.743 6.743 0 0 0-.426-3.293Zm-7.556 5.132c.03.015.059.03.09.044l.005.003a3.257 3.257 0 0 1-.095-.047Zm6.906-4.79v-.006l.001.007h-.001Z" fill="url(#a)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.315-1.66a6.81 6.81 0 0 1 .664 1.99V37.895a5.937 5.937 0 0 1-.203 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093a4.88 4.88 0 0 0-.105.905v.034a6.557 6.557 0 0 0 13.016 1.108c.011-.084.02-.168.03-.254a6.742 6.742 0 0 0-.425-3.293h-.001Z" fill="url(#b)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.315-1.66a6.81 6.81 0 0 1 .664 1.99V37.895a5.937 5.937 0 0 1-.203 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093a4.88 4.88 0 0 0-.105.905v.034a6.557 6.557 0 0 0 13.016 1.108c.011-.084.02-.168.03-.254a6.742 6.742 0 0 0-.425-3.293h-.001Z" fill="url(#c)"/><path d="m42.879 38.322.04.03a3.567 3.567 0 0 0-.608-.795c-2.038-2.038-.534-4.418-.28-4.54l.002-.004c-1.647.965-2.206 2.75-2.257 3.642.077-.005.153-.011.23-.011a3.308 3.308 0 0 1 2.873 1.677Z" fill="url(#d)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.864 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.074.036.11.053.065.029.129.055.193.079.275.097.562.152.853.164 3.268.154 3.9-3.907 1.543-5.086a2.263 2.263 0 0 1 1.58.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#e)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.864 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.074.036.11.053.065.029.129.055.193.079.275.097.562.152.853.164 3.268.154 3.9-3.907 1.543-5.086a2.263 2.263 0 0 1 1.58.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#f)"/><path d="M37.666 37.134c.053.034.096.063.135.09a3.04 3.04 0 0 1-.019-1.602 4.854 4.854 0 0 0-1.578 1.22c.032-.002.983-.019 1.462.292Z" fill="url(#g)"/><path d="M33.503 40.145c.506 2.989 3.216 5.272 6.291 5.359 2.847.08 4.665-1.572 5.416-3.184a5.937 5.937 0 0 0 .204-4.425v-.012l.002.01c.232 1.52-.54 2.99-1.748 3.986l-.004.008c-2.352 1.917-4.604 1.156-5.059.846a3.524 3.524 0 0 1-.095-.047c-1.372-.656-1.939-1.905-1.817-2.977a1.684 1.684 0 0 1-1.553-.977 2.474 2.474 0 0 1 2.41-.097c.777.352 1.66.387 2.462.097-.003-.054-1.144-.508-1.589-.946-.237-.234-.35-.347-.45-.432a1.973 1.973 0 0 0-.17-.128 9.602 9.602 0 0 0-.135-.09c-.478-.31-1.429-.294-1.46-.293h-.003c-.26-.33-.242-1.415-.227-1.642-.077.03-.15.07-.219.116-.23.164-.444.348-.64.55a5.72 5.72 0 0 0-.616.733 5.538 5.538 0 0 0-.88 1.986c-.003.013-.236 1.031-.121 1.56l.001-.001Z" fill="url(#h)"/><path d="M42.31 37.557c.24.235.445.503.61.795.035.027.07.054.098.08 1.486 1.37.707 3.307.65 3.444 1.207-.994 1.978-2.466 1.747-3.985-.742-1.849-2-2.595-3.025-4.217a7.809 7.809 0 0 1-.154-.25 2.078 2.078 0 0 1-.072-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.011-.006.023.023 0 0 0-.013 0l-.003.002-.004.003c-.254.12-1.758 2.501.28 4.538v.003Z" fill="url(#i)"/><path d="M43.018 38.433a1.351 1.351 0 0 0-.099-.08l-.04-.03a2.263 2.263 0 0 0-1.581-.384c2.358 1.179 1.725 5.239-1.543 5.086a2.916 2.916 0 0 1-.853-.164 3.497 3.497 0 0 1-.192-.08c-.037-.016-.074-.033-.11-.052l.004.003c.455.311 2.706 1.071 5.06-.846l.003-.008c.058-.137.837-2.074-.65-3.444Z" fill="url(#j)"/><path d="M37.055 40.581s.302-1.127 2.167-1.127c.201 0 .778-.563.788-.726a3.265 3.265 0 0 1-2.461-.097 2.471 2.471 0 0 0-2.41.097 1.684 1.684 0 0 0 1.552.977c-.121 1.072.446 2.32 1.817 2.977l.091.045c-.8-.414-1.462-1.196-1.544-2.145Z" fill="url(#k)"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.316-1.66a6.81 6.81 0 0 1 .664 1.99l.002.01c-.742-1.847-1.998-2.593-3.025-4.215a8.23 8.23 0 0 1-.154-.25 2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.023.023 0 0 0-.013 0l-.003.002-.005.003.003-.005c-1.647.964-2.206 2.749-2.256 3.642.076-.005.152-.011.23-.011a3.307 3.307 0 0 1 2.872 1.677 2.263 2.263 0 0 0-1.58-.384c2.357 1.18 1.725 5.239-1.543 5.087a2.916 2.916 0 0 1-.853-.165 3.495 3.495 0 0 1-.193-.079c-.037-.017-.074-.033-.11-.053l.004.003a3.524 3.524 0 0 1-.095-.047l.09.044c-.8-.413-1.461-1.195-1.544-2.144 0 0 .303-1.128 2.167-1.128.202 0 .779-.563.789-.726-.003-.053-1.144-.507-1.589-.945-.237-.234-.35-.347-.45-.432a1.988 1.988 0 0 0-.17-.128 3.04 3.04 0 0 1-.018-1.603 4.854 4.854 0 0 0-1.578 1.22h-.003c-.26-.33-.241-1.415-.226-1.642-.077.03-.15.07-.22.116-.229.164-.443.347-.64.549-.224.227-.43.473-.613.735v.001-.001a5.538 5.538 0 0 0-.88 1.986l-.008.043c-.013.058-.068.35-.076.414-.05.302-.083.607-.097.914v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.253a6.742 6.742 0 0 0-.426-3.294Zm-.65.337v.006-.007Z" fill="url(#l)"/><defs><radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(44.777 34.536) scale(13.6677)"><stop offset=".129" stop-color="#FFBD4F"/><stop offset=".186" stop-color="#FFAC31"/><stop offset=".247" stop-color="#FF9D17"/><stop offset=".283" stop-color="#FF980E"/><stop offset=".403" stop-color="#FF563B"/><stop offset=".467" stop-color="#FF3750"/><stop offset=".71" stop-color="#F5156C"/><stop offset=".782" stop-color="#EB0878"/><stop offset=".86" stop-color="#E50080"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.702 40.118) scale(13.6677)"><stop offset=".3" stop-color="#960E18"/><stop offset=".351" stop-color="#B11927" stop-opacity=".74"/><stop offset=".435" stop-color="#DB293D" stop-opacity=".343"/><stop offset=".497" stop-color="#F5334B" stop-opacity=".094"/><stop offset=".53" stop-color="#FF3750" stop-opacity="0"/></radialGradient><radialGradient id="d" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(41.394 31.491) scale(9.90066)"><stop offset=".132" stop-color="#FFF44F"/><stop offset=".252" stop-color="#FFDC3E"/><stop offset=".506" stop-color="#FF9D12"/><stop offset=".526" stop-color="#FF980E"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(38.18 43.67) scale(6.50741)"><stop offset=".353" stop-color="#3A8EE6"/><stop offset=".472" stop-color="#5C79F0"/><stop offset=".669" stop-color="#9059FF"/><stop offset="1" stop-color="#C139E6"/></radialGradient><radialGradient id="f" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.35414 -.81093 .9494 3.92687 40.363 38.945)"><stop offset=".206" stop-color="#9059FF" stop-opacity="0"/><stop offset=".278" stop-color="#8C4FF3" stop-opacity=".064"/><stop offset=".747" stop-color="#7716A8" stop-opacity=".45"/><stop offset=".975" stop-color="#6E008B" stop-opacity=".6"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 34.028) scale(4.68221)"><stop stop-color="#FFE226"/><stop offset=".121" stop-color="#FFDB27"/><stop offset=".295" stop-color="#FFC82A"/><stop offset=".502" stop-color="#FFA930"/><stop offset=".732" stop-color="#FF7E37"/><stop offset=".792" stop-color="#FF7139"/></radialGradient><radialGradient id="h" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.254 30.983) scale(19.9772)"><stop offset=".113" stop-color="#FFF44F"/><stop offset=".456" stop-color="#FF980E"/><stop offset=".622" stop-color="#FF5634"/><stop offset=".716" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="i" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.53635 14.55876 -9.55479 1.0083 41.594 32.091)"><stop stop-color="#FFF44F"/><stop offset=".06" stop-color="#FFE847"/><stop offset=".168" stop-color="#FFC830"/><stop offset=".304" stop-color="#FF980E"/><stop offset=".356" stop-color="#FF8B16"/><stop offset=".455" stop-color="#FF672A"/><stop offset=".57" stop-color="#FF3647"/><stop offset=".737" stop-color="#E31587"/></radialGradient><radialGradient id="j" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 35.72) scale(12.4701)"><stop offset=".137" stop-color="#FFF44F"/><stop offset=".48" stop-color="#FF980E"/><stop offset=".592" stop-color="#FF5634"/><stop offset=".655" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="k" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(42.747 36.397) scale(13.6491)"><stop offset=".094" stop-color="#FFF44F"/><stop offset=".231" stop-color="#FFE141"/><stop offset=".509" stop-color="#FFAF1E"/><stop offset=".626" stop-color="#FF980E"/></radialGradient><linearGradient id="a" x1="45.198" y1="35.108" x2="34.314" y2="45.609" gradientUnits="userSpaceOnUse"><stop offset=".048" stop-color="#FFF44F"/><stop offset=".111" stop-color="#FFE847"/><stop offset=".225" stop-color="#FFC830"/><stop offset=".368" stop-color="#FF980E"/><stop offset=".401" stop-color="#FF8B16"/><stop offset=".462" stop-color="#FF672A"/><stop offset=".534" stop-color="#FF3647"/><stop offset=".705" stop-color="#E31587"/></linearGradient><linearGradient id="l" x1="45.066" y1="35.053" x2="35.806" y2="44.314" gradientUnits="userSpaceOnUse"><stop offset=".167" stop-color="#FFF44F" stop-opacity=".8"/><stop offset=".266" stop-color="#FFF44F" stop-opacity=".634"/><stop offset=".489" stop-color="#FFF44F" stop-opacity=".217"/><stop offset=".6" stop-color="#FFF44F" stop-opacity="0"/></linearGradient></defs></svg>
\ No newline at end of file diff --git a/browser/components/preferences/moreFromMozilla.inc.xhtml b/browser/components/preferences/moreFromMozilla.inc.xhtml new file mode 100644 index 0000000000..6db36392e0 --- /dev/null +++ b/browser/components/preferences/moreFromMozilla.inc.xhtml @@ -0,0 +1,44 @@ +# 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/. + +<!-- More From Mozilla panel --> + +<script src="chrome://browser/content/preferences/moreFromMozilla.js"/> +<html:template xmlns="http://www.w3.org/1999/xhtml" id="template-paneMoreFromMozilla"> + <div id="moreFromMozillaCategory-header" + class="subcategory" + data-hidden-from-search="true" + hidden="true" + data-category="paneMoreFromMozilla"> + <h1 class="title" data-l10n-id="more-from-moz-title"/> + <p class="subtitle" data-l10n-id="more-from-moz-subtitle"/> + </div> + <div id="moreFromMozillaCategory" + data-category="paneMoreFromMozilla" + hidden="true" + data-hidden-from-search="true"> + </div> +</html:template> + +<html:template xmlns="http://www.w3.org/1999/xhtml" id="simple"> + <article class="mozilla-product-item simple"> + + <div> + <h2 class="product-title"/> + <div class="product-description-box"> + <div class="description"/> + <a class="text-link" target="_blank" hidden="true"/> + </div> + </div> + + <button type="button" class="primary small-button" hidden="true"/> + + <div class="qr-code-box" hidden="true"> + <h3 class="qr-code-box-title"/> + <img class="qr-code-box-image"/> + <a class="qr-code-link text-link" target="_blank" hidden="true"/> + </div> + + </article> +</html:template> diff --git a/browser/components/preferences/moreFromMozilla.js b/browser/components/preferences/moreFromMozilla.js new file mode 100644 index 0000000000..f9cd0c8a82 --- /dev/null +++ b/browser/components/preferences/moreFromMozilla.js @@ -0,0 +1,271 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from preferences.js */ + +var gMoreFromMozillaPane = { + initialized: false, + + /** + * "default" is whatever template is the default, as defined by the code + * in this file (currently in `getTemplateName`). Setting option to an + * invalid value will leave it unchanged. + */ + _option: "default", + set option(value) { + if (!value) { + this._option = "default"; + return; + } + + if (value === "default" || value === "simple") { + this._option = value; + } + }, + + get option() { + return this._option; + }, + + getTemplateName() { + if (!this._option || this._option == "default") { + return "simple"; + } + return this._option; + }, + + getURL(url, region, option, hasEmail) { + const URL_PARAMS = { + utm_source: "about-prefs", + utm_campaign: "morefrommozilla", + utm_medium: "firefox-desktop", + }; + // UTM content param used in analytics to record + // UI template used to open URL + const utm_content = { + default: "default", + simple: "fxvt-113-a", + }; + + const experiment_params = { + entrypoint_experiment: "morefrommozilla-experiment-1846", + }; + + let pageUrl = new URL(url); + for (let [key, val] of Object.entries(URL_PARAMS)) { + pageUrl.searchParams.append(key, val); + } + + // Append region by product to utm_content and also + // append '-email' when URL is opened + // from send email link in QRCode box + if (option) { + pageUrl.searchParams.set( + "utm_content", + `${utm_content[option]}-${region}${hasEmail ? "-email" : ""}` + ); + } + + // Add experiments params when user is shown an experimental UI + // with template value as 'simple' set via Nimbus + if (option !== "default") { + pageUrl.searchParams.set( + "entrypoint_experiment", + experiment_params.entrypoint_experiment + ); + pageUrl.searchParams.set("entrypoint_variation", `treatment-${option}`); + } + return pageUrl.toString(); + }, + + renderProducts() { + let products = [ + { + id: "firefox-mobile", + title_string_id: "more-from-moz-firefox-mobile-title", + description_string_id: "more-from-moz-firefox-mobile-description", + region: "global", + button: { + id: "fxMobile", + type: "link", + label_string_id: "more-from-moz-learn-more-link", + actionURL: AppConstants.isChinaRepack() + ? "https://www.firefox.com.cn/browsers/mobile/" + : "https://www.mozilla.org/firefox/browsers/mobile/", + }, + qrcode: { + title: { + string_id: "more-from-moz-qr-code-box-firefox-mobile-title", + }, + image_src_prefix: + "chrome://browser/content/preferences/more-from-mozilla-qr-code", + button: { + id: "qr-code-send-email", + label: { + string_id: "more-from-moz-qr-code-box-firefox-mobile-button", + }, + actionURL: AppConstants.isChinaRepack() + ? "https://www.firefox.com.cn/mobile/get-app/" + : "https://www.mozilla.org/firefox/mobile/get-app/?v=mfm", + }, + }, + }, + ]; + + if (BrowserUtils.shouldShowVPNPromo()) { + const vpn = { + id: "mozilla-vpn", + title_string_id: "more-from-moz-mozilla-vpn-title", + description_string_id: "more-from-moz-mozilla-vpn-description", + region: "global", + button: { + id: "mozillaVPN", + label_string_id: "more-from-moz-button-mozilla-vpn-2", + actionURL: "https://www.mozilla.org/products/vpn/", + }, + }; + products.push(vpn); + } + + if (BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY)) { + const relay = { + id: "firefox-relay", + title_string_id: "more-from-moz-firefox-relay-title", + description_string_id: "more-from-moz-firefox-relay-description", + region: "global", + button: { + id: "firefoxRelay", + label_string_id: "more-from-moz-firefox-relay-button", + actionURL: "https://relay.firefox.com/", + }, + }; + products.push(relay); + } + + this._productsContainer = document.getElementById( + "moreFromMozillaCategory" + ); + let frag = document.createDocumentFragment(); + this._template = document.getElementById(this.getTemplateName()); + + // Exit when internal data is incomplete + if (!this._template) { + return; + } + + for (let product of products) { + let template = this._template.content.cloneNode(true); + let title = template.querySelector(".product-title"); + let desc = template.querySelector(".description"); + + title.setAttribute("data-l10n-id", product.title_string_id); + title.id = product.id; + + desc.setAttribute("data-l10n-id", product.description_string_id); + + let isLink = product.button.type === "link"; + let actionElement = template.querySelector( + isLink ? ".text-link" : ".small-button" + ); + + if (actionElement) { + actionElement.hidden = false; + actionElement.id = `${this.option}-${product.button.id}`; + document.l10n.setAttributes( + actionElement, + product.button.label_string_id + ); + + if (isLink) { + actionElement.setAttribute( + "href", + this.getURL(product.button.actionURL, product.region, this.option) + ); + } else { + actionElement.addEventListener("click", function () { + let mainWindow = window.windowRoot.ownerGlobal; + mainWindow.openTrustedLinkIn( + gMoreFromMozillaPane.getURL( + product.button.actionURL, + product.region, + gMoreFromMozillaPane.option + ), + "tab" + ); + }); + } + } + + if (product.qrcode) { + let qrcode = template.querySelector(".qr-code-box"); + qrcode.setAttribute("hidden", "false"); + + let qrcode_title = template.querySelector(".qr-code-box-title"); + qrcode_title.setAttribute( + "data-l10n-id", + product.qrcode.title.string_id + ); + + let img = template.querySelector(".qr-code-box-image"); + // Append QRCode image source by template. For CN region + // simple template, we want a CN specific QRCode + img.src = + product.qrcode.image_src_prefix + + "-" + + this.getTemplateName() + + `${ + AppConstants.isChinaRepack() && + this.getTemplateName().includes("simple") + ? "-cn" + : "" + }` + + ".svg"; + // Add image a11y attributes + img.setAttribute( + "data-l10n-id", + "more-from-moz-qr-code-firefox-mobile-img" + ); + + let qrc_link = template.querySelector(".qr-code-link"); + + // So the telemetry includes info about which option is being used + qrc_link.id = `${this.option}-${product.qrcode.button.id}`; + + // For supported locales, this link allows users to send themselves a + // download link by email. It should be hidden for unsupported locales. + if (BrowserUtils.sendToDeviceEmailsSupported()) { + qrc_link.setAttribute( + "data-l10n-id", + product.qrcode.button.label.string_id + ); + qrc_link.href = this.getURL( + product.qrcode.button.actionURL, + product.region, + this.option, + true + ); + qrc_link.hidden = false; + } + } + + frag.appendChild(template); + } + this._productsContainer.appendChild(frag); + }, + + async init() { + if (this.initialized) { + return; + } + this.initialized = true; + document + .getElementById("moreFromMozillaCategory") + .removeAttribute("data-hidden-from-search"); + document + .getElementById("moreFromMozillaCategory-header") + .removeAttribute("data-hidden-from-search"); + + this.renderProducts(); + }, +}; diff --git a/browser/components/preferences/moz.build b/browser/components/preferences/moz.build new file mode 100644 index 0000000000..54f7b042fb --- /dev/null +++ b/browser/components/preferences/moz.build @@ -0,0 +1,23 @@ +# -*- 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/. + +DIRS += ["dialogs"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser.ini", "tests/siteData/browser.ini"] + +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 + +if CONFIG["MOZ_UPDATE_AGENT"]: + DEFINES["MOZ_UPDATE_AGENT"] = True + +JAR_MANIFESTS += ["jar.mn"] + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Settings UI") diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js new file mode 100644 index 0000000000..8498a5cfb7 --- /dev/null +++ b/browser/components/preferences/preferences.js @@ -0,0 +1,661 @@ +/* - 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 the files imported by the .xul files. +/* import-globals-from main.js */ +/* import-globals-from home.js */ +/* import-globals-from search.js */ +/* import-globals-from containers.js */ +/* import-globals-from privacy.js */ +/* import-globals-from sync.js */ +/* import-globals-from experimental.js */ +/* import-globals-from moreFromMozilla.js */ +/* import-globals-from findInPage.js */ +/* import-globals-from /browser/base/content/utilityOverlay.js */ +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +"use strict"; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" +); +var { Integration } = ChromeUtils.importESModule( + "resource://gre/modules/Integration.sys.mjs" +); +/* global DownloadIntegration */ +Integration.downloads.defineESModuleGetter( + this, + "DownloadIntegration", + "resource://gre/modules/DownloadIntegration.sys.mjs" +); + +var { PrivateBrowsingUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); + +var { Weave } = ChromeUtils.importESModule( + "resource://services-sync/main.sys.mjs" +); + +var { FirefoxRelayTelemetry } = ChromeUtils.importESModule( + "resource://gre/modules/FirefoxRelayTelemetry.mjs" +); + +var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +var fxAccounts = getFxAccountsSingleton(); + +XPCOMUtils.defineLazyServiceGetters(this, { + gApplicationUpdateService: [ + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService", + ], + + listManager: [ + "@mozilla.org/url-classifier/listmanager;1", + "nsIUrlListManager", + ], + gHandlerService: [ + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService", + ], + gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"], +}); + +if (Cc["@mozilla.org/gio-service;1"]) { + XPCOMUtils.defineLazyServiceGetter( + this, + "gGIOService", + "@mozilla.org/gio-service;1", + "nsIGIOService" + ); +} else { + this.gGIOService = null; +} + +ChromeUtils.defineESModuleGetters(this, { + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", + ContextualIdentityService: + "resource://gre/modules/ContextualIdentityService.sys.mjs", + DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", + ExtensionPreferencesManager: + "resource://gre/modules/ExtensionPreferencesManager.sys.mjs", + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", + FeatureGate: "resource://featuregates/FeatureGate.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs", + LangPackMatcher: "resource://gre/modules/LangPackMatcher.sys.mjs", + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", + UIState: "resource://services-sync/UIState.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HomePage: "resource:///modules/HomePage.jsm", + SelectionChangedMenulist: "resource:///modules/SelectionChangedMenulist.jsm", + SiteDataManager: "resource:///modules/SiteDataManager.jsm", + TransientPrefs: "resource:///modules/TransientPrefs.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () { + const { SubDialogManager } = ChromeUtils.importESModule( + "resource://gre/modules/SubDialog.sys.mjs" + ); + return new SubDialogManager({ + dialogStack: document.getElementById("dialogStack"), + dialogTemplate: document.getElementById("dialogTemplate"), + dialogOptions: { + styleSheets: [ + "chrome://browser/skin/preferences/dialog.css", + "chrome://browser/skin/preferences/preferences.css", + ], + resizeCallback: async ({ title, frame }) => { + // Search within main document and highlight matched keyword. + await gSearchResultsPane.searchWithinNode( + title, + gSearchResultsPane.query + ); + + // Search within sub-dialog document and highlight matched keyword. + await gSearchResultsPane.searchWithinNode( + frame.contentDocument.firstElementChild, + gSearchResultsPane.query + ); + + // Creating tooltips for all the instances found + for (let node of gSearchResultsPane.listSearchTooltips) { + if (!node.tooltipNode) { + gSearchResultsPane.createSearchTooltip( + node, + gSearchResultsPane.query + ); + } + } + }, + }, + }); +}); + +var gLastCategory = { category: undefined, subcategory: undefined }; +const gXULDOMParser = new DOMParser(); +var gCategoryModules = new Map(); +var gCategoryInits = new Map(); +function init_category_if_required(category) { + let categoryInfo = gCategoryInits.get(category); + if (!categoryInfo) { + throw new Error( + "Unknown in-content prefs category! Can't init " + category + ); + } + if (categoryInfo.inited) { + return null; + } + return categoryInfo.init(); +} + +function register_module(categoryName, categoryObject) { + gCategoryModules.set(categoryName, categoryObject); + gCategoryInits.set(categoryName, { + inited: false, + async init() { + let startTime = performance.now(); + let template = document.getElementById("template-" + categoryName); + if (template) { + // Replace the template element with the nodes inside of it. + let frag = template.content; + await document.l10n.translateFragment(frag); + + // Actually insert them into the DOM. + document.l10n.pauseObserving(); + template.replaceWith(frag); + document.l10n.resumeObserving(); + + // We need to queue an update again because the previous update might + // have happened while we awaited on translateFragment. + Preferences.queueUpdateOfAllElements(); + } + + categoryObject.init(); + this.inited = true; + ChromeUtils.addProfilerMarker( + "Preferences", + { startTime }, + categoryName + " init" + ); + }, + }); +} + +document.addEventListener("DOMContentLoaded", init_all, { once: true }); + +function init_all() { + Preferences.forceEnableInstantApply(); + + // Asks Preferences to queue an update of the attribute values of + // the entire document. + Preferences.queueUpdateOfAllElements(); + Services.telemetry.setEventRecordingEnabled("aboutpreferences", true); + + register_module("paneGeneral", gMainPane); + register_module("paneHome", gHomePane); + register_module("paneSearch", gSearchPane); + register_module("panePrivacy", gPrivacyPane); + register_module("paneContainers", gContainersPane); + if (Services.prefs.getBoolPref("browser.preferences.experimental")) { + // Set hidden based on previous load's hidden value. + document.getElementById("category-experimental").hidden = + Services.prefs.getBoolPref( + "browser.preferences.experimental.hidden", + false + ); + register_module("paneExperimental", gExperimentalPane); + } + + NimbusFeatures.moreFromMozilla.recordExposureEvent({ once: true }); + if (NimbusFeatures.moreFromMozilla.getVariable("enabled")) { + document.getElementById("category-more-from-mozilla").hidden = false; + gMoreFromMozillaPane.option = + NimbusFeatures.moreFromMozilla.getVariable("template"); + register_module("paneMoreFromMozilla", gMoreFromMozillaPane); + } + // The Sync category needs to be the last of the "real" categories + // registered and inititalized since many tests wait for the + // "sync-pane-loaded" observer notification before starting the test. + if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) { + document.getElementById("category-sync").hidden = false; + register_module("paneSync", gSyncPane); + } + register_module("paneSearchResults", gSearchResultsPane); + gSearchResultsPane.init(); + gMainPane.preInit(); + + let categories = document.getElementById("categories"); + categories.addEventListener("select", event => gotoPref(event.target.value)); + + document.documentElement.addEventListener("keydown", function (event) { + if (event.keyCode == KeyEvent.DOM_VK_TAB) { + categories.setAttribute("keyboard-navigation", "true"); + } + }); + categories.addEventListener("mousedown", function () { + this.removeAttribute("keyboard-navigation"); + }); + + maybeDisplayPoliciesNotice(); + + window.addEventListener("hashchange", onHashChange); + + document.getElementById("focusSearch1").addEventListener("command", () => { + gSearchResultsPane.searchInput.focus(); + }); + + gotoPref().then(() => { + document.getElementById("addonsButton").addEventListener("click", e => { + e.preventDefault(); + if (e.button >= 2) { + // Ignore right clicks. + return; + } + let mainWindow = window.browsingContext.topChromeWindow; + mainWindow.BrowserOpenAddonsMgr(); + }); + + document.dispatchEvent( + new CustomEvent("Initialized", { + bubbles: true, + cancelable: true, + }) + ); + }); +} + +function telemetryBucketForCategory(category) { + category = category.toLowerCase(); + switch (category) { + case "containers": + case "general": + case "home": + case "privacy": + case "search": + case "sync": + case "searchresults": + return category; + default: + return "unknown"; + } +} + +function onHashChange() { + gotoPref(null, "hash"); +} + +async function gotoPref( + aCategory, + aShowReason = aCategory ? "click" : "initial" +) { + let categories = document.getElementById("categories"); + const kDefaultCategoryInternalName = "paneGeneral"; + const kDefaultCategory = "general"; + let hash = document.location.hash; + let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName; + + let breakIndex = category.indexOf("-"); + // Subcategories allow for selecting smaller sections of the preferences + // until proper search support is enabled (bug 1353954). + let subcategory = breakIndex != -1 && category.substring(breakIndex + 1); + if (subcategory) { + category = category.substring(0, breakIndex); + } + category = friendlyPrefCategoryNameToInternalName(category); + if (category != "paneSearchResults") { + gSearchResultsPane.query = null; + gSearchResultsPane.searchInput.value = ""; + gSearchResultsPane.removeAllSearchIndicators(window, true); + } else if (!gSearchResultsPane.searchInput.value) { + // Something tried to send us to the search results pane without + // a query string. Default to the General pane instead. + category = kDefaultCategoryInternalName; + document.location.hash = kDefaultCategory; + gSearchResultsPane.query = null; + } + + // Updating the hash (below) or changing the selected category + // will re-enter gotoPref. + if (gLastCategory.category == category && !subcategory) { + return; + } + + let item; + if (category != "paneSearchResults") { + // Hide second level headers in normal view + for (let element of document.querySelectorAll(".search-header")) { + element.hidden = true; + } + + item = categories.querySelector(".category[value=" + category + "]"); + if (!item || item.hidden) { + category = kDefaultCategoryInternalName; + item = categories.querySelector(".category[value=" + category + "]"); + } + } + + if ( + gLastCategory.category || + category != kDefaultCategoryInternalName || + subcategory + ) { + let friendlyName = internalPrefCategoryNameToFriendlyName(category); + // Overwrite the hash, unless there is no hash and we're switching to the + // default category, e.g. by using the 'back' button after navigating to + // a different category. + if ( + !(!document.location.hash && category == kDefaultCategoryInternalName) + ) { + document.location.hash = friendlyName; + } + } + // Need to set the gLastCategory before setting categories.selectedItem since + // the categories 'select' event will re-enter the gotoPref codepath. + gLastCategory.category = category; + gLastCategory.subcategory = subcategory; + if (item) { + categories.selectedItem = item; + } else { + categories.clearSelection(); + } + window.history.replaceState(category, document.title); + + try { + await init_category_if_required(category); + } catch (ex) { + console.error( + new Error( + "Error initializing preference category " + category + ": " + ex + ) + ); + throw ex; + } + + // Bail out of this goToPref if the category + // or subcategory changed during async operation. + if ( + gLastCategory.category !== category || + gLastCategory.subcategory !== subcategory + ) { + return; + } + + search(category, "data-category"); + + if (aShowReason != "initial") { + document.querySelector(".main-content").scrollTop = 0; + } + + // Check to see if the category module wants to do any special + // handling of the subcategory - for example, opening a SubDialog. + // + // If not, just do a normal spotlight on the subcategory. + let categoryModule = gCategoryModules.get(category); + if (!categoryModule.handleSubcategory?.(subcategory)) { + spotlight(subcategory, category); + } + + // Record which category is shown + Services.telemetry.recordEvent( + "aboutpreferences", + "show", + aShowReason, + category + ); +} + +function search(aQuery, aAttribute) { + let mainPrefPane = document.getElementById("mainPrefPane"); + let elements = mainPrefPane.children; + for (let element of elements) { + // If the "data-hidden-from-search" is "true", the + // element will not get considered during search. + if ( + element.getAttribute("data-hidden-from-search") != "true" || + element.getAttribute("data-subpanel") == "true" + ) { + let attributeValue = element.getAttribute(aAttribute); + if (attributeValue == aQuery) { + element.hidden = false; + } else { + element.hidden = true; + } + } else if ( + element.getAttribute("data-hidden-from-search") == "true" && + !element.hidden + ) { + element.hidden = true; + } + element.classList.remove("visually-hidden"); + } + + let keysets = mainPrefPane.getElementsByTagName("keyset"); + for (let element of keysets) { + let attributeValue = element.getAttribute(aAttribute); + if (attributeValue == aQuery) { + element.removeAttribute("disabled"); + } else { + element.setAttribute("disabled", true); + } + } +} + +async function spotlight(subcategory, category) { + let highlightedElements = document.querySelectorAll(".spotlight"); + if (highlightedElements.length) { + for (let element of highlightedElements) { + element.classList.remove("spotlight"); + } + } + if (subcategory) { + scrollAndHighlight(subcategory, category); + } +} + +async function scrollAndHighlight(subcategory, category) { + let element = document.querySelector(`[data-subcategory="${subcategory}"]`); + if (!element) { + return; + } + let header = getClosestDisplayedHeader(element); + + scrollContentTo(header); + element.classList.add("spotlight"); +} + +/** + * If there is no visible second level header it will return first level header, + * otherwise return second level header. + * @returns {Element} - The closest displayed header. + */ +function getClosestDisplayedHeader(element) { + let header = element.closest("groupbox"); + let searchHeader = header.querySelector(".search-header"); + if ( + searchHeader && + searchHeader.hidden && + header.previousElementSibling.classList.contains("subcategory") + ) { + header = header.previousElementSibling; + } + return header; +} + +function scrollContentTo(element) { + const STICKY_CONTAINER_HEIGHT = + document.querySelector(".sticky-container").clientHeight; + let mainContent = document.querySelector(".main-content"); + let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT; + mainContent.scroll({ + top, + behavior: "smooth", + }); +} + +function friendlyPrefCategoryNameToInternalName(aName) { + if (aName.startsWith("pane")) { + return aName; + } + return "pane" + aName.substring(0, 1).toUpperCase() + aName.substr(1); +} + +// This function is duplicated inside of utilityOverlay.js's openPreferences. +function internalPrefCategoryNameToFriendlyName(aName) { + return (aName || "").replace(/^pane./, function (toReplace) { + return toReplace[4].toLowerCase(); + }); +} + +// Put up a confirm dialog with "ok to restart", "revert without restarting" +// and "restart later" buttons and returns the index of the button chosen. +// We can choose not to display the "restart later", or "revert" buttons, +// altough the later still lets us revert by using the escape key. +// +// The constants are useful to interpret the return value of the function. +const CONFIRM_RESTART_PROMPT_RESTART_NOW = 0; +const CONFIRM_RESTART_PROMPT_CANCEL = 1; +const CONFIRM_RESTART_PROMPT_RESTART_LATER = 2; +async function confirmRestartPrompt( + aRestartToEnable, + aDefaultButtonIndex, + aWantRevertAsCancelButton, + aWantRestartLaterButton +) { + let [ + msg, + title, + restartButtonText, + noRestartButtonText, + restartLaterButtonText, + ] = await document.l10n.formatValues([ + { + id: aRestartToEnable + ? "feature-enable-requires-restart" + : "feature-disable-requires-restart", + }, + { id: "should-restart-title" }, + { id: "should-restart-ok" }, + { id: "cancel-no-restart-button" }, + { id: "restart-later" }, + ]); + + // Set up the first (index 0) button: + let buttonFlags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING; + + // Set up the second (index 1) button: + if (aWantRevertAsCancelButton) { + buttonFlags += + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING; + } else { + noRestartButtonText = null; + buttonFlags += + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL; + } + + // Set up the third (index 2) button: + if (aWantRestartLaterButton) { + buttonFlags += + Services.prompt.BUTTON_POS_2 * Services.prompt.BUTTON_TITLE_IS_STRING; + } else { + restartLaterButtonText = null; + } + + switch (aDefaultButtonIndex) { + case 0: + buttonFlags += Services.prompt.BUTTON_POS_0_DEFAULT; + break; + case 1: + buttonFlags += Services.prompt.BUTTON_POS_1_DEFAULT; + break; + case 2: + buttonFlags += Services.prompt.BUTTON_POS_2_DEFAULT; + break; + default: + break; + } + + let buttonIndex = Services.prompt.confirmEx( + window, + title, + msg, + buttonFlags, + restartButtonText, + noRestartButtonText, + restartLaterButtonText, + null, + {} + ); + + // If we have the second confirmation dialog for restart, see if the user + // cancels out at that point. + if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + if (cancelQuit.data) { + buttonIndex = CONFIRM_RESTART_PROMPT_CANCEL; + } + } + return buttonIndex; +} + +// This function is used to append search keywords found +// in the related subdialog to the button that will activate the subdialog. +function appendSearchKeywords(aId, keywords) { + let element = document.getElementById(aId); + let searchKeywords = element.getAttribute("searchkeywords"); + if (searchKeywords) { + keywords.push(searchKeywords); + } + element.setAttribute("searchkeywords", keywords.join(" ")); +} + +async function ensureScrollPadding() { + let stickyContainer = document.querySelector(".sticky-container"); + let height = await window.browsingContext.topChromeWindow + .promiseDocumentFlushed(() => stickyContainer.clientHeight) + .catch(err => Cu.reportError); // Can reject if the window goes away. + + // Make it a bit more, to ensure focus rectangles etc. don't get cut off. + // This being 8px causes us to end up with 90px if the policies container + // is not visible (the common case), which matches the CSS and thus won't + // cause a style change, repaint, or other changes. + height += 8; + stickyContainer + .closest(".main-content") + .style.setProperty("scroll-padding-top", height + "px"); +} + +function maybeDisplayPoliciesNotice() { + if (Services.policies.status == Services.policies.ACTIVE) { + document.getElementById("policies-container").removeAttribute("hidden"); + ensureScrollPadding(); + } +} diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml new file mode 100644 index 0000000000..2976864c80 --- /dev/null +++ b/browser/components/preferences/preferences.xhtml @@ -0,0 +1,244 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/handlers.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/search.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/containers.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?> + +<!DOCTYPE html> + +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:html="http://www.w3.org/1999/xhtml" + role="document" + id="preferences-root"> + +<head> + <!-- @CSP: We should remove 'unsafe-inline' from style-src, see Bug 1579160 --> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" /> + + <title data-l10n-id="settings-page-title"></title> + + <meta name="color-scheme" content="light dark" /> + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="browser/browser.ftl"/> + <!-- Used by fontbuilder.js --> + <link rel="localization" href="browser/preferences/fonts.ftl"/> + <link rel="localization" href="browser/preferences/moreFromMozilla.ftl"/> + <link rel="localization" href="browser/preferences/preferences.ftl"/> + <link rel="localization" href="toolkit/branding/accounts.ftl"/> + <link rel="localization" href="toolkit/branding/brandings.ftl"/> + <link rel="localization" href="toolkit/featuregates/features.ftl"/> + + <!-- Links below are only used for search-l10n-ids into subdialogs --> + <link rel="localization" href="browser/aboutDialog.ftl"/> + <link rel="localization" href="browser/preferences/addEngine.ftl"/> + <link rel="localization" href="browser/preferences/blocklists.ftl"/> + <link rel="localization" href="browser/preferences/clearSiteData.ftl"/> + <link rel="localization" href="browser/preferences/colors.ftl"/> + <link rel="localization" href="browser/preferences/connection.ftl"/> + <link rel="localization" href="browser/preferences/formAutofill.ftl"/> + <link rel="localization" href="browser/preferences/languages.ftl"/> + <link rel="localization" href="browser/preferences/permissions.ftl"/> + <link rel="localization" href="browser/preferences/selectBookmark.ftl"/> + <link rel="localization" href="browser/preferences/siteDataSettings.ftl"/> + <link rel="localization" href="browser/sanitize.ftl"/> + <link rel="localization" href="browser/translations.ftl"/> + <link rel="localization" href="preview/firefoxSuggest.ftl"/> + <link rel="localization" href="security/certificates/certManager.ftl"/> + <link rel="localization" href="security/certificates/deviceManager.ftl"/> + <link rel="localization" href="toolkit/updates/history.ftl"/> + + <link rel="shortcut icon" href="chrome://global/skin/icons/settings.svg"/> + + <script src="chrome://browser/content/utilityOverlay.js"/> + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://browser/content/preferences/preferences.js"/> + <script src="chrome://browser/content/preferences/extensionControlled.js"/> + <script src="chrome://browser/content/preferences/findInPage.js"/> + <script type="module" src="chrome://global/content/elements/moz-support-link.mjs"/> + <script src="chrome://browser/content/migration/migration-wizard.mjs" type="module"></script> + <script type="module" src="chrome://global/content/elements/moz-toggle.mjs"/> +</head> + +<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="preferences-body"> + + <stringbundle id="pkiBundle" + src="chrome://pippki/locale/pippki.properties"/> + <stringbundle id="browserBundle" + src="chrome://browser/locale/browser.properties"/> + + <stack id="preferences-stack" flex="1"> + <hbox flex="1"> + + <vbox class="navigation"> + <!-- category list --> + <richlistbox id="categories" data-l10n-id="category-list" data-l10n-attrs="aria-label"> + <richlistitem id="category-general" + class="category" + value="paneGeneral" + helpTopic="prefs-main" + data-l10n-id="category-general" + data-l10n-attrs="tooltiptext" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="pane-general-title"></label> + </richlistitem> + + <richlistitem id="category-home" + class="category" + value="paneHome" + helpTopic="prefs-home" + data-l10n-id="category-home" + data-l10n-attrs="tooltiptext" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="pane-home-title"></label> + </richlistitem> + + <richlistitem id="category-search" + class="category" + value="paneSearch" + helpTopic="prefs-search" + data-l10n-id="category-search" + data-l10n-attrs="tooltiptext" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="pane-search-title"></label> + </richlistitem> + + <!-- hidden with CSS; this is only here to allow the containers pane to + be switched to using the URL or the "Settings..." button. --> + <richlistitem id="category-containers" + class="category" + value="paneContainers" + helpTopic="prefs-containers"/> + + <richlistitem id="category-privacy" + class="category" + value="panePrivacy" + helpTopic="prefs-privacy" + data-l10n-id="category-privacy" + data-l10n-attrs="tooltiptext" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="pane-privacy-title"></label> + </richlistitem> + + <richlistitem id="category-sync" + class="category" + hidden="true" + value="paneSync" + helpTopic="prefs-weave" + data-l10n-id="category-sync3" + data-l10n-attrs="tooltiptext" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="pane-sync-title3"></label> + </richlistitem> + + <richlistitem id="category-experimental" + class="category" + hidden="true" + value="paneExperimental" + helpTopic="prefs-experimental" + data-l10n-id="category-experimental" + data-l10n-attrs="tooltiptext" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="pane-experimental-title"></label> + </richlistitem> + <richlistitem id="category-more-from-mozilla" + class="category" + hidden="true" + data-l10n-id="more-from-moz-category" + data-l10n-attrs="tooltiptext" + value="paneMoreFromMozilla" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="more-from-moz-title"></label> + </richlistitem> + </richlistbox> + + <spacer flex="1"/> + + <vbox class="sidebar-footer-list"> + <html:a id="addonsButton" class="sidebar-footer-link" href="about:addons"> + <image class="sidebar-footer-icon addons-icon"/> + <label class="sidebar-footer-label" flex="1" data-l10n-id="addons-button-label"></label> + </html:a> + <html:a id="helpButton" class="sidebar-footer-link" target="_blank" + is="moz-support-link" support-page="preferences"> + <image class="sidebar-footer-icon help-icon"/> + <label class="sidebar-footer-label" flex="1" data-l10n-id="help-button-label"></label> + </html:a> + </vbox> + </vbox> + + <keyset> + <key data-l10n-id="focus-search" key="" modifiers="accel" id="focusSearch1"/> + </keyset> + + <vbox class="main-content" flex="1" align="start"> + <vbox class="pane-container"> + <hbox class="sticky-container"> + <hbox class="sticky-inner-container" pack="end" align="start"> + <hbox id="policies-container" class="info-box-container smaller-font-size" flex="1" hidden="true"> + <hbox class="info-icon-container"> + <html:img class="info-icon"></html:img> + </hbox> + <hbox align="center" flex="1"> + <html:a href="about:policies" target="_blank" data-l10n-id="managed-notice"/> + </hbox> + </hbox> + <search-textbox + id="searchInput" + data-l10n-id="search-input-box2" + data-l10n-attrs="placeholder, style" + hidden="true"/> + </hbox> + </hbox> + <vbox id="mainPrefPane"> +#include searchResults.inc.xhtml +#include main.inc.xhtml +#include home.inc.xhtml +#include search.inc.xhtml +#include privacy.inc.xhtml +#include containers.inc.xhtml +#include sync.inc.xhtml +#include experimental.inc.xhtml +#include moreFromMozilla.inc.xhtml + </vbox> + </vbox> + </vbox> + </hbox> + + <stack id="dialogStack" hidden="true"/> + <vbox id="dialogTemplate" class="dialogOverlay" align="center" pack="center" topmost="true" hidden="true"> + <vbox class="dialogBox" + pack="end" + role="dialog" + aria-labelledby="dialogTitle"> + <hbox class="dialogTitleBar" align="center"> + <label class="dialogTitle" flex="1"/> + <button class="dialogClose close-icon" + data-l10n-id="close-button"/> + </hbox> + <browser class="dialogFrame" + autoscroll="false" + disablehistory="true"/> + </vbox> + </vbox> + </stack> + + <html:dialog id="migrationWizardDialog"></html:dialog> +</html:body> +</html> diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml new file mode 100644 index 0000000000..aea4c26913 --- /dev/null +++ b/browser/components/preferences/privacy.inc.xhtml @@ -0,0 +1,1380 @@ +# 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/. + +<!-- Privacy panel --> + +<script src="chrome://browser/content/preferences/privacy.js"/> +<stringbundle id="signonBundle" src="chrome://passwordmgr/locale/passwordmgr.properties"/> +<html:template id="template-panePrivacy"> +<hbox id="browserPrivacyCategory" + class="subcategory" + hidden="true" + data-category="panePrivacy"> + <html:h1 data-l10n-id="privacy-header"/> +</hbox> + +<!-- Tracking / Content Blocking --> +<groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" aria-describedby="contentBlockingDescription" class="highlighting-group"> + <label id="contentBlockingHeader"><html:h2 data-l10n-id="content-blocking-enhanced-tracking-protection"/></label> + <vbox data-subcategory="trackingprotection"> + <hbox align="start"> + <image id="trackingProtectionShield"/> + <description class="description-with-side-element" flex="1"> + <html:span id="contentBlockingDescription" class="tail-with-learn-more" data-l10n-id="content-blocking-section-top-level-description"></html:span> + <html:a is="moz-support-link" + id="contentBlockingLearnMore" + class="learnMore" + data-l10n-id="content-blocking-learn-more" + support-page="enhanced-tracking-protection" + /> + </description> + <button id="trackingProtectionExceptions" + is="highlightable-button" + class="accessory-button" + data-l10n-id="tracking-manage-exceptions" + preference="pref.privacy.disable_button.tracking_protection_exceptions" + search-l10n-ids=" + permissions-address, + permissions-disable-etp, + permissions-remove.label, + permissions-remove-all.label, + permissions-exceptions-etp-window2.title, + permissions-exceptions-manage-etp-desc, + "/> + </hbox> + <hbox id="fpiIncompatibilityWarning" class="info-box-container" hidden="true"> + <vbox class="info-icon-container"> + <html:img class="info-icon"></html:img> + </vbox> + <vbox flex="1"> + <description> + <html:span data-l10n-id="content-blocking-fpi-incompatibility-warning"/> + </description> + </vbox> + </hbox> + <vbox id="contentBlockingCategories"> + <radiogroup id="contentBlockingCategoryRadio" + preference="browser.contentblocking.category" + aria-labelledby="trackingProtectionMenuDesc"> + <vbox id="contentBlockingOptionStandard" class="privacy-detailedoption info-box-container"> + <hbox> + <radio id="standardRadio" + value="standard" + data-l10n-id="enhanced-tracking-protection-setting-standard" + flex="1"/> + <button id="standardArrow" + is="highlightable-button" + class="arrowhead" + data-l10n-id="content-blocking-expand-section" + aria-expanded="false"/> + </hbox> + <vbox class="indent"> + <description data-l10n-id="content-blocking-etp-standard-desc"></description> + <vbox class="privacy-extra-information"> + <label class="content-blocking-extra-blocking-desc" data-l10n-id="content-blocking-etp-blocking-desc"/> + <vbox class="indent"> + <hbox class="extra-information-label social-media-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-social-media-trackers"/> + </hbox> + <hbox class="extra-information-label cross-site-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-cookies-in-all-windows2"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies"/> + </hbox> + <hbox class="extra-information-label all-third-party-cookies-private-windows-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies-private-windows"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-plus-isolate-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies-plus-isolate"/> + </hbox> + <hbox class="extra-information-label pb-trackers-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-private-windows"/> + </hbox> + <hbox class="extra-information-label trackers-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-windows-tracking-content"/> + </hbox> + <hbox class="extra-information-label all-third-party-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies"/> + </hbox> + <hbox class="extra-information-label all-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-cookies"/> + </hbox> + <hbox class="extra-information-label unvisited-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-unvisited-cookies"/> + </hbox> + <hbox class="extra-information-label cryptominers-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cryptominers"/> + </hbox> + <hbox class="extra-information-label fingerprinters-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-fingerprinters"/> + </hbox> + </vbox> + <vbox id="etpStandardTCPBox" class="content-blocking-warning info-box-container"> + <label class="content-blocking-warning-title" data-l10n-id="content-blocking-etp-standard-tcp-title"/> + <description> + <html:span class="tail-with-learn-more" data-l10n-id="content-blocking-etp-standard-tcp-rollout-description"></html:span> + <html:a is="moz-support-link" + id="tcp-learn-more-link" + class="learnMore" + data-l10n-id="content-blocking-etp-standard-tcp-rollout-learn-more" + support-page="total-cookie-protection" + /> + </description> + </vbox> + <html:div class="content-blocking-warning info-box-container reload-tabs" hidden="true"> + <html:div class="content-blocking-reload-desc-container"> + <html:div class="info-icon-container"> + <html:img class="info-icon"/> + </html:div> + <html:span data-l10n-id="content-blocking-reload-description" + class="content-blocking-reload-description" /> + </html:div> + <button class="accessory-button reload-tabs-button primary" + is="highlightable-button" + data-l10n-id="content-blocking-reload-tabs-button"/> + </html:div> + </vbox> + </vbox> + </vbox> + <vbox id="contentBlockingOptionStrict" class="privacy-detailedoption info-box-container"> + <hbox> + <radio id="strictRadio" + value="strict" + data-l10n-id="enhanced-tracking-protection-setting-strict" + flex="1"/> + <button id="strictArrow" + is="highlightable-button" + class="arrowhead" + data-l10n-id="content-blocking-expand-section" + aria-expanded="false"/> + </hbox> + <vbox class="indent"> + <label data-l10n-id="content-blocking-etp-strict-desc"></label> + <vbox class="privacy-extra-information"> + <label class="content-blocking-extra-blocking-desc" data-l10n-id="content-blocking-etp-blocking-desc"/> + <vbox class="indent"> + <hbox class="extra-information-label social-media-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-social-media-trackers"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies"/> + </hbox> + <hbox class="extra-information-label all-third-party-cookies-private-windows-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies-private-windows"/> + </hbox> + <hbox class="extra-information-label cross-site-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-cookies-in-all-windows2"/> + </hbox> + <hbox class="extra-information-label pb-trackers-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-private-windows"/> + </hbox> + <hbox class="extra-information-label trackers-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-windows-tracking-content"/> + </hbox> + <hbox class="extra-information-label all-third-party-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies"/> + </hbox> + <hbox class="extra-information-label all-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-all-cookies"/> + </hbox> + <hbox class="extra-information-label unvisited-cookies-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-unvisited-cookies"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-plus-isolate-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies-plus-isolate"/> + </hbox> + <hbox class="extra-information-label cryptominers-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-cryptominers"/> + </hbox> + <hbox class="extra-information-label fingerprinters-option" hidden="true"> + <label class="content-blocking-label" data-l10n-id="content-blocking-fingerprinters"/> + </hbox> + </vbox> + <html:div class="content-blocking-warning info-box-container reload-tabs" hidden="true"> + <html:div class="content-blocking-reload-desc-container"> + <html:div class="info-icon-container"> + <html:img class="info-icon"/> + </html:div> + <html:span data-l10n-id="content-blocking-reload-description" + class="content-blocking-reload-description" /> + </html:div> + <button class="accessory-button reload-tabs-button primary" + is="highlightable-button" + data-l10n-id="content-blocking-reload-tabs-button"/> + </html:div> + <vbox class="content-blocking-warning info-box-container"> + <hbox> + <image class="content-blocking-warning-image"/> + <label class="content-blocking-warning-title" data-l10n-id="content-blocking-warning-title"/> + </hbox> + <description class="indent"> + <html:span class="tail-with-learn-more content-blocking-warning-description" data-l10n-id="content-blocking-and-isolating-etp-warning-description-2"></html:span> + <html:a is="moz-support-link" + class="learnMore" + data-l10n-id="content-blocking-warning-learn-how" + support-page="turn-off-etp-desktop" + /> + </description> + </vbox> + </vbox> + </vbox> + </vbox> + <vbox id="contentBlockingOptionCustom" class="privacy-detailedoption info-box-container"> + <hbox> + <radio id="customRadio" + value="custom" + data-l10n-id="enhanced-tracking-protection-setting-custom" + flex="1"/> + <button id="customArrow" + is="highlightable-button" + class="arrowhead" + data-l10n-id="content-blocking-expand-section" + aria-expanded="false"/> + </hbox> + <vbox class="indent"> + <description id="contentBlockingCustomDesc" data-l10n-id="content-blocking-etp-custom-desc"></description> + <vbox class="privacy-extra-information"> + <hbox class="reject-trackers-ui custom-option"> + <checkbox id="contentBlockingBlockCookiesCheckbox" + class="content-blocking-checkbox" flex="1" + data-l10n-id="content-blocking-cookies-label" + aria-describedby="contentBlockingCustomDesc" + preference="network.cookie.cookieBehavior"/> + <vbox> + <menulist id="blockCookiesMenu" + sizetopopup="none" + preference="network.cookie.cookieBehavior"> + <menupopup> + <menuitem id="blockCookiesSocialMedia" data-l10n-id="sitedata-option-block-cross-site-trackers" value="trackers"/> + <menuitem id="isolateCookiesSocialMedia" data-l10n-id="sitedata-option-block-cross-site-cookies" value="trackers-plus-isolate"/> + <menuitem data-l10n-id="sitedata-option-block-unvisited" value="unvisited"/> + <menuitem data-l10n-id="sitedata-option-block-all-cross-site-cookies" value="all-third-parties"/> + <menuitem data-l10n-id="sitedata-option-block-all" value="always"/> + </menupopup> + </menulist> + </vbox> + </hbox> + <hbox id="contentBlockingTrackingProtectionExtensionContentLabel" + align="center" hidden="true" class="extension-controlled"> + <description control="contentBlockingDisableTrackingProtectionExtension" flex="1"/> + <button id="contentBlockingDisableTrackingProtectionExtension" + is="highlightable-button" + class="extension-controlled-button accessory-button" + data-l10n-id="disable-extension" hidden="true"/> + </hbox> + <hbox class="custom-option"> + <checkbox id="contentBlockingTrackingProtectionCheckbox" + class="content-blocking-checkbox" flex="1" + data-l10n-id="content-blocking-tracking-content-label" + aria-describedby="contentBlockingCustomDesc"/> + <vbox> + <menulist id="trackingProtectionMenu"> + <menupopup> + <menuitem data-l10n-id="content-blocking-option-private" value="private"/> + <menuitem data-l10n-id="content-blocking-tracking-protection-option-all-windows" value="always"/> + </menupopup> + </menulist> + </vbox> + </hbox> + <label id="changeBlockListLink" + data-l10n-id="content-blocking-tracking-protection-change-block-list" + is="text-link" + search-l10n-ids="blocklist-window2.title, blocklist-description, blocklist-dialog.buttonlabelaccept"/> + + <hbox class="custom-option" id="contentBlockingCryptominersOption"> + <checkbox id="contentBlockingCryptominersCheckbox" + class="content-blocking-checkbox" flex="1" + preference="privacy.trackingprotection.cryptomining.enabled" + data-l10n-id="content-blocking-cryptominers-label" + aria-describedby="contentBlockingCustomDesc"/> + </hbox> + <hbox class="custom-option" id="contentBlockingFingerprintersOption"> + <checkbox id="contentBlockingFingerprintersCheckbox" + class="content-blocking-checkbox" flex="1" + preference="privacy.trackingprotection.fingerprinting.enabled" + data-l10n-id="content-blocking-fingerprinters-label" + aria-describedby="contentBlockingCustomDesc"/> + </hbox> + <html:div class="content-blocking-warning info-box-container reload-tabs" hidden="true"> + <html:div class="content-blocking-reload-desc-container"> + <html:div class="info-icon-container"> + <html:img class="info-icon"/> + </html:div> + <html:span data-l10n-id="content-blocking-reload-description" + class="content-blocking-reload-description" /> + </html:div> + <button class="accessory-button reload-tabs-button primary" + is="highlightable-button" + data-l10n-id="content-blocking-reload-tabs-button"/> + </html:div> + <vbox class="content-blocking-warning info-box-container"> + <hbox> + <image class="content-blocking-warning-image"/> + <label class="content-blocking-warning-title" data-l10n-id="content-blocking-warning-title"/> + </hbox> + <description class="indent"> + <html:span class="tail-with-learn-more content-blocking-warning-description" data-l10n-id="content-blocking-and-isolating-etp-warning-description-2"></html:span> + <html:a is="moz-support-link" + class="learnMore" + data-l10n-id="content-blocking-warning-learn-how" + support-page="turn-off-etp-desktop" + /> + </description> + </vbox> + </vbox> + </vbox> + </vbox> + </radiogroup> + </vbox> + <vbox id="doNotTrackLearnMoreBox"> + <label> + <label class="tail-with-learn-more" data-l10n-id="do-not-track-description" id="doNotTrackDesc"></label> + <html:a is="moz-support-link" + class="learnMore" + data-l10n-id="do-not-track-learn-more" + support-page="how-do-i-turn-do-not-track-feature" + /> + </label> + <radiogroup id="doNotTrackRadioGroup" aria-labelledby="doNotTrackDesc" preference="privacy.donottrackheader.enabled"> + <radio value="true" data-l10n-id="do-not-track-option-always"/> + <radio value="false" data-l10n-id="do-not-track-option-default-content-blocking-known"/> + </radiogroup> + </vbox> + </vbox> +</groupbox> + +<!-- Site Data --> +<groupbox id="siteDataGroup" data-category="panePrivacy" hidden="true" aria-describedby="totalSiteDataSize"> + <label><html:h2 data-l10n-id="sitedata-header"/></label> + + <hbox data-subcategory="sitedata" align="baseline"> + <vbox flex="1"> + <description class="description-with-side-element" flex="1"> + <html:span id="totalSiteDataSize" class="tail-with-learn-more"></html:span> + <html:a is="moz-support-link" + id="siteDataLearnMoreLink" + data-l10n-id="sitedata-learn-more" + support-page="storage-permissions" + /> + </description> + <hbox flex="1" id="deleteOnCloseNote" class="info-box-container smaller-font-size"> + <hbox class="info-icon-container"> + <html:img class="info-icon"></html:img> + </hbox> + <description flex="1" data-l10n-id="sitedata-delete-on-close-private-browsing" /> + </hbox> + <hbox id="keepRow" + align="center"> + <checkbox id="deleteOnClose" + data-l10n-id="sitedata-delete-on-close" + flex="1" /> + </hbox> + </vbox> + <vbox align="end"> + <button id="clearSiteDataButton" + is="highlightable-button" + class="accessory-button" + search-l10n-ids="clear-site-data-cookies-empty.label, clear-site-data-cache-empty.label" + data-l10n-id="sitedata-clear"/> + <button id="siteDataSettings" + is="highlightable-button" + class="accessory-button" + data-l10n-id="sitedata-settings" + search-l10n-ids=" + site-data-settings-window.title, + site-data-column-host.label, + site-data-column-cookies.label, + site-data-column-storage.label, + site-data-settings-description, + site-data-remove-all.label, + "/> + <button id="cookieExceptions" + is="highlightable-button" + class="accessory-button" + data-l10n-id="sitedata-cookies-exceptions" + preference="pref.privacy.disable_button.cookie_exceptions" + search-l10n-ids=" + permissions-address, + permissions-block.label, + permissions-allow.label, + permissions-remove.label, + permissions-remove-all.label, + permissions-exceptions-cookie-desc, + " /> + </vbox> + </hbox> +</groupbox> + +<!-- Cookie Banner Handling --> +<groupbox id="cookieBannerHandlingGroup" data-category="panePrivacy" data-subcategory="cookiebanner" hidden="true"> + <label><html:h2 data-l10n-id="cookie-banner-handling-header" /></label> + <vbox flex="1"> + <hbox> + <description> + <html:span id="cookieBannerReductionExplanation" class="tail-with-learn-more" data-l10n-id="cookie-banner-handling-description" ></html:span> + <html:a is="moz-support-link" id="cookieBannerHandlingLearnMore" class="learnMore" data-l10n-id="cookie-banner-learn-more" support-page="cookie-banner-reduction"/> + </description> + </hbox> + <hbox> + <checkbox id="handleCookieBanners" + preference="cookiebanners.service.mode" + data-l10n-id="forms-handle-cookie-banners" + flex="1" /> + </hbox> + </vbox> +</groupbox> + +<!-- Passwords --> +<groupbox id="passwordsGroup" orient="vertical" data-category="panePrivacy" data-subcategory="logins" hidden="true"> + <label><html:h2 data-l10n-id="pane-privacy-logins-and-passwords-header" data-l10n-attrs="searchkeywords"/></label> + + <vbox id="passwordSettings"> + <hbox id="passwordManagerExtensionContent" + class="extension-controlled" + align="center" + hidden="true"> + <description control="disablePasswordManagerExtension" + flex="1"/> + <button id="disablePasswordManagerExtension" + class="extension-controlled-button accessory-button" + data-l10n-id="disable-extension" + hidden="true" /> + </hbox> + <hbox> + <vbox flex="1"> + <hbox> + <checkbox id="savePasswords" + data-l10n-id="forms-ask-to-save-logins" + preference="signon.rememberSignons" + flex="1" /> + </hbox> + <hbox class="indent" flex="1"> + <checkbox id="passwordAutofillCheckbox" + data-l10n-id="forms-fill-logins-and-passwords" + search-l10n-id="forms-fill-logins-and-passwords.label" + preference="signon.autofillForms" + flex="1" /> + </hbox> + <hbox class="indent" id="generatePasswordsBox" flex="1"> + <checkbox id="generatePasswords" + data-l10n-id="forms-generate-passwords" + search-l10n-ids="forms-generate-passwords.label" + preference="signon.generation.enabled" + flex="1" /> + </hbox> + </vbox> + <vbox align="end"> + <button id="passwordExceptions" + is="highlightable-button" + class="accessory-button" + data-l10n-id="forms-exceptions" + preference="pref.privacy.disable_button.view_passwords_exceptions" + search-l10n-ids=" + permissions-address, + permissions-exceptions-saved-logins-window2.title, + permissions-exceptions-saved-logins-desc, + "/> + <button id="showPasswords" + is="highlightable-button" + class="accessory-button" + data-l10n-id="forms-saved-logins" + search-l10n-ids="forms-saved-logins.label" + preference="pref.privacy.disable_button.view_passwords"/> + </vbox> + </hbox> + <hbox class="indent" id="relayIntegrationBox" flex="1" align="center"> + <checkbox id="relayIntegration" + class="tail-with-learn-more" + data-l10n-id="preferences-relay-integration-checkbox" + search-l10n-ids="preferences-relay-integration-checkbox.label" /> + <html:a id="relayIntegrationLearnMoreLink" class="learnMore" + data-l10n-id="relay-integration-learn-more-link"/> + </hbox> + <hbox class="indent" id="breachAlertsBox" flex="1" align="center"> + <checkbox id="breachAlerts" + class="tail-with-learn-more" + data-l10n-id="forms-breach-alerts" + search-l10n-ids="breach-alerts.label" + preference="signon.management.page.breach-alerts.enabled"/> + <html:a is="moz-support-link" + id="breachAlertsLearnMoreLink" + data-l10n-id="forms-breach-alerts-learn-more-link" + support-page="lockwise-alerts" + /> + </hbox> + </vbox> + <vbox> + <hbox id="masterPasswordRow" align="center"> + <checkbox id="useMasterPassword" + data-l10n-id="forms-primary-pw-use" + class="tail-with-learn-more"/> + <html:a is="moz-support-link" + id="primaryPasswordLearnMoreLink" + data-l10n-id="forms-primary-pw-learn-more-link" + support-page="primary-password-stored-logins" + /> + <spacer flex="1"/> + <button id="changeMasterPassword" + is="highlightable-button" + class="accessory-button" + search-l10n-ids="forms-master-pw-change.label" + data-l10n-id="forms-primary-pw-change"/> + </hbox> + <description class="indent tip-caption" + data-l10n-id="forms-primary-pw-former-name" + data-l10n-attrs="hidden" + flex="1"/> +#ifdef XP_WIN + <hbox id="windows-sso" align="center"> + <checkbox data-l10n-id="forms-windows-sso" + preference="network.http.windows-sso.enabled" + class="tail-with-learn-more"/> + <html:a is="moz-support-link" + id="windowsSSOLearnMoreLink" + data-l10n-id="forms-windows-sso-learn-more-link" + support-page="windows-sso" + /> + </hbox> + <description id="windows-sso-caption" class="indent tip-caption" + data-l10n-id="forms-windows-sso-desc"/> + +#endif + </vbox> + <!-- + Those two strings are meant to be invisible and will be used exclusively to provide + localization for an alert window. + --> + <label id="fips-title" hidden="true" data-l10n-id="forms-primary-pw-fips-title"></label> + <label id="fips-desc" hidden="true" data-l10n-id="forms-master-pw-fips-desc"></label> +</groupbox> + +<!-- The form autofill section is inserted in to this box + after the form autofill extension has initialized. --> +<groupbox id="formAutofillGroupBox" + data-category="panePrivacy" + data-subcategory="form-autofill" hidden="true"></groupbox> + +<!-- History --> +<groupbox id="historyGroup" data-category="panePrivacy" hidden="true"> + <label><html:h2 data-l10n-id="history-header"/></label> + <hbox align="center"> + <label id="historyModeLabel" + control="historyMode" + data-l10n-id="history-remember-label"/> + <menulist id="historyMode"> + <menupopup> + <menuitem data-l10n-id="history-remember-option-all" + value="remember" + search-l10n-ids="history-remember-description"/> + <menuitem data-l10n-id="history-remember-option-never" + value="dontremember" + search-l10n-ids="history-dontremember-description"/> + <menuitem data-l10n-id="history-remember-option-custom" + value="custom" + search-l10n-ids=" + history-private-browsing-permanent.label, + history-remember-browser-option.label, + history-remember-search-option.label, + history-clear-on-close-option.label, + history-clear-on-close-settings.label"/> + </menupopup> + </menulist> + </hbox> + <hbox> + <deck id="historyPane" flex="1"> + <vbox id="historyRememberPane"> + <hbox align="center" flex="1"> + <vbox flex="1"> + <description + class="description-with-side-element" + data-l10n-id="history-remember-description"/> + </vbox> + </hbox> + </vbox> + <vbox id="historyDontRememberPane"> + <hbox align="center" flex="1"> + <vbox flex="1"> + <description + class="description-with-side-element" + data-l10n-id="history-dontremember-description"/> + </vbox> + </hbox> + </vbox> + <vbox id="historyCustomPane"> + <vbox> + <checkbox id="privateBrowsingAutoStart" + data-l10n-id="history-private-browsing-permanent" + preference="browser.privatebrowsing.autostart"/> + <vbox class="indent"> + <checkbox id="rememberHistory" + data-l10n-id="history-remember-browser-option" + preference="places.history.enabled"/> + <checkbox id="rememberForms" + data-l10n-id="history-remember-search-option" + preference="browser.formfill.enable"/> + <hbox id="clearDataBox" + align="center"> + <checkbox id="alwaysClear" + preference="privacy.sanitize.sanitizeOnShutdown" + data-l10n-id="history-clear-on-close-option" + flex="1" /> + </hbox> + </vbox> + </vbox> + </vbox> + </deck> + <vbox id="historyButtons" align="end"> + <button id="clearHistoryButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="history-clear-button"/> + <button id="clearDataSettings" + is="highlightable-button" + class="accessory-button" + data-l10n-id="history-clear-on-close-settings" + search-l10n-ids=" + clear-data-settings-label, + history-section-label, + item-history-and-downloads.label, + item-cookies.label, + item-active-logins.label, + item-cache.label, + item-form-search-history.label, + data-section-label, + item-site-settings.label, + item-offline-apps.label + "/> + </vbox> + </hbox> +</groupbox> + +<!-- Address Bar --> +<groupbox id="locationBarGroup" + data-category="panePrivacy" + hidden="true" + data-subcategory="locationBar"> + <label><html:h2 id="locationBarGroupHeader" data-l10n-id="addressbar-header"/></label> + <label id="locationBarSuggestionLabel" data-l10n-id="addressbar-suggest"/> + <checkbox id="historySuggestion" data-l10n-id="addressbar-locbar-history-option" + preference="browser.urlbar.suggest.history"/> + <checkbox id="bookmarkSuggestion" data-l10n-id="addressbar-locbar-bookmarks-option" + preference="browser.urlbar.suggest.bookmark"/> + <checkbox id="openpageSuggestion" data-l10n-id="addressbar-locbar-openpage-option" + preference="browser.urlbar.suggest.openpage"/> + <checkbox id="topSitesSuggestion" + data-l10n-id="addressbar-locbar-shortcuts-option" + preference="browser.urlbar.suggest.topsites"/> + <checkbox id="enginesSuggestion" data-l10n-id="addressbar-locbar-engines-option" + preference="browser.urlbar.suggest.engines"/> + <hbox id="firefoxSuggestBestMatchContainer" align="center" hidden="true"> + <checkbox id="firefoxSuggestBestMatch" + class="tail-with-learn-more" + data-l10n-id="addressbar-firefox-suggest-best-match-option" + preference="browser.urlbar.suggest.bestmatch"/> + <html:a is="moz-support-link" + id="firefoxSuggestBestMatchLearnMore" + class="learnMore" + data-l10n-id="addressbar-best-match-learn-more" + support-page="firefox-suggest" + /> + </hbox> + <hbox id="quickActionsBox" align="center" hidden="true"> + <checkbox id="enableQuickActions" + class="tail-with-learn-more" + data-l10n-id="addressbar-locbar-quickactions-option" + preference="browser.urlbar.suggest.quickactions" /> + <html:a is="moz-support-link" + id="quickActionsLink" + data-l10n-id="addressbar-quickactions-learn-more" + support-page="quick-actions-firefox-search-bar" + /> + </hbox> + <vbox id="firefoxSuggestContainer" hidden="true"> + <vbox class="firefoxSuggestOptionBox"> + <html:moz-toggle id="firefoxSuggestNonsponsoredToggle" + preference="browser.urlbar.suggest.quicksuggest.nonsponsored" + data-l10n-id="addressbar-firefox-suggest-nonsponsored" + data-l10n-attrs="label, description" /> + </vbox> + <vbox class="firefoxSuggestOptionBox"> + <html:moz-toggle id="firefoxSuggestSponsoredToggle" + preference="browser.urlbar.suggest.quicksuggest.sponsored" + data-l10n-id="addressbar-firefox-suggest-sponsored" + data-l10n-attrs="label, description" /> + </vbox> + <vbox class="firefoxSuggestOptionBox"> + <html:moz-toggle id="firefoxSuggestDataCollectionToggle" + preference="browser.urlbar.quicksuggest.dataCollection.enabled" + data-l10n-id="addressbar-firefox-suggest-data-collection" + data-l10n-attrs="label, description"> + <html:a slot="support-link" + is="moz-support-link" + id="firefoxSuggestDataCollectionLearnMore" + class="learnMore firefoxSuggestLearnMore" + data-l10n-id="addressbar-locbar-firefox-suggest-learn-more" + support-page="firefox-suggest" + /> + </html:moz-toggle> + </vbox> + <hbox id="firefoxSuggestInfoBox" class="info-box-container smaller-font-size" align="start" + hidden="true"> + <html:img class="info-icon"/> + <description flex="1"> + <html:span id="firefoxSuggestInfoText" class="tail-with-learn-more"/> + <html:a is="moz-support-link" + id="firefoxSuggestInfoBoxLearnMore" + class="learnMore firefoxSuggestLearnMore" + data-l10n-id="addressbar-locbar-firefox-suggest-learn-more" + support-page="firefox-suggest" + /> + </description> + </hbox> + <hbox id="dismissedSuggestions" align="center"> + <vbox flex="1"> + <label data-l10n-id="addressbar-dismissed-suggestions-label"/> + <description class="tip-caption"> + <html:span id="dismissedSuggestionsDescription" + class="tail-with-learn-more" + data-l10n-id="addressbar-restore-dismissed-suggestions-description"/> + <html:a is="moz-support-link" + id="dismissedSuggestionsLearnMore" + class="learnMore firefoxSuggestLearnMore" + data-l10n-id="addressbar-restore-dismissed-suggestions-learn-more" + support-page="firefox-suggest" + /> + </description> + </vbox> + <button id="restoreDismissedSuggestions" + is="highlightable-button" + class="accessory-button" + data-l10n-id="addressbar-restore-dismissed-suggestions-button" + aria-describedby="dismissedSuggestionsDescription"/> + </hbox> + </vbox> + <label id="openSearchEnginePreferences" is="text-link" + data-l10n-id="addressbar-suggestions-settings"/> +</groupbox> + +<hbox id="permissionsCategory" + class="subcategory" + hidden="true" + data-category="panePrivacy"> + <html:h1 data-l10n-id="permissions-header"/> +</hbox> + +<!-- Permissions --> +<groupbox id="permissionsGroup" data-category="panePrivacy" hidden="true" data-subcategory="permissions"> + <label class="search-header" hidden="true"><html:h2 data-l10n-id="permissions-header"/></label> + + <!-- The hbox around the buttons is to compute the search tooltip position properly --> + <vbox> + <hbox id="locationSettingsRow" align="center" role="group" aria-labelledby="locationPermissionsLabel"> + <description flex="1"> + <image class="geo-icon permission-icon" /> + <label id="locationPermissionsLabel" data-l10n-id="permissions-location"/> + </description> + <hbox pack="end"> + <button id="locationSettingsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-location-settings" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-site-location-window2.title, + permissions-site-location-desc, + permissions-site-location-disable-label, + permissions-site-location-disable-desc, + " /> + </hbox> + </hbox> + + <hbox id="cameraSettingsRow" align="center" role="group" aria-labelledby="cameraPermissionsLabel"> + <description flex="1"> + <image class="camera-icon permission-icon" /> + <label id="cameraPermissionsLabel" data-l10n-id="permissions-camera"/> + </description> + <hbox pack="end"> + <button id="cameraSettingsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-camera-settings" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-site-camera-window2.title, + permissions-site-camera-desc, + permissions-site-camera-disable-label, + permissions-site-camera-disable-desc, + " /> + </hbox> + </hbox> + + <hbox id="microphoneSettingsRow" align="center" role="group" aria-labelledby="microphonePermissionsLabel"> + <description flex="1"> + <image class="microphone-icon permission-icon" /> + <label id="microphonePermissionsLabel" data-l10n-id="permissions-microphone"/> + </description> + <hbox pack="end"> + <button id="microphoneSettingsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-microphone-settings" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-site-microphone-window2.title, + permissions-site-microphone-desc, + permissions-site-microphone-disable-label, + permissions-site-microphone-disable-desc, + " /> + </hbox> + </hbox> + + <hbox id="speakerSettingsRow" align="center" role="group" aria-labelledby="speakerPermissionsLabel"> + <description flex="1"> + <image class="speaker-icon permission-icon" /> + <label id="speakerPermissionsLabel" data-l10n-id="permissions-speaker"/> + </description> + <hbox pack="end"> + <button id="speakerSettingsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-speaker-settings" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-site-speaker-window.title, + permissions-site-speaker-desc, + " /> + </hbox> + </hbox> + + <hbox id="notificationSettingsRow" align="center" role="group" aria-labelledby="notificationPermissionsLabel"> + <description flex="1"> + <image class="desktop-notification-icon permission-icon" /> + <label id="notificationPermissionsLabel" + class="tail-with-learn-more" + data-l10n-id="permissions-notification"/> + <html:a is="moz-support-link" + id="notificationPermissionsLearnMore" + class="learnMore" + data-l10n-id="permissions-notification-link" + support-page="push" + /> + </description> + <hbox pack="end"> + <button id="notificationSettingsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-notification-settings" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-site-notification-window2.title, + permissions-site-notification-desc, + permissions-site-notification-disable-label, + permissions-site-notification-disable-desc, + " /> + </hbox> + </hbox> + + <vbox id="notificationsDoNotDisturbBox" hidden="true"> + <checkbox id="notificationsDoNotDisturb" class="indent"/> + </vbox> + + <hbox id="autoplaySettingsRow" align="center" role="group" aria-labelledby="autoplayPermissionsLabel"> + <description flex="1"> + <image class="autoplay-icon permission-icon" /> + <label id="autoplayPermissionsLabel" + data-l10n-id="permissions-autoplay"/> + </description> + <hbox pack="end"> + <button id="autoplaySettingsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-autoplay-settings" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-site-autoplay-window2.title, + permissions-site-autoplay-desc, + " /> + </hbox> + </hbox> + + <hbox id="xrSettingsRow" align="center" role="group" aria-labelledby="xrPermissionsLabel"> + <description flex="1"> + <image class="xr-icon permission-icon" /> + <label id="xrPermissionsLabel" data-l10n-id="permissions-xr"/> + </description> + <hbox pack="end"> + <button id="xrSettingsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-xr-settings" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-site-xr-window2.title, + permissions-site-xr-desc, + permissions-site-xr-disable-label, + permissions-site-xr-disable-desc, + " /> + </hbox> + </hbox> + </vbox> + + <separator /> + + <hbox data-subcategory="permissions-block-popups"> + <checkbox id="popupPolicy" preference="dom.disable_open_during_load" + data-l10n-id="permissions-block-popups" + flex="1" /> + <button id="popupPolicyButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-block-popups-exceptions-button" + data-l10n-attrs="searchkeywords" + search-l10n-ids=" + permissions-address, + permissions-exceptions-popup-window2.title, + permissions-exceptions-popup-desc, + " /> + </hbox> + + <hbox id="addonInstallBox"> + <checkbox id="warnAddonInstall" + data-l10n-id="permissions-addon-install-warning" + preference="xpinstall.whitelist.required" + flex="1" /> + <button id="addonExceptions" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-addon-exceptions" + search-l10n-ids=" + permissions-address, + permissions-allow.label, + permissions-remove.label, + permissions-remove-all.label, + permissions-exceptions-addons-window2.title, + permissions-exceptions-addons-desc, + " /> + </hbox> + +</groupbox> + +<!-- Firefox Data Collection and Use --> +<hbox id="dataCollectionCategory" + class="subcategory" + hidden="true" + data-category="panePrivacy"> + <html:h1 data-l10n-id="collection-header"/> +</hbox> + +<groupbox id="dataCollectionGroup" data-category="panePrivacy" hidden="true"> + <label class="search-header" hidden="true"><html:h2 data-l10n-id="collection-header"/></label> + + <description> + <label class="tail-with-learn-more" data-l10n-id="collection-description"/> + <html:a id="dataCollectionPrivacyNotice" + class="learnMore" + data-l10n-id="collection-privacy-notice"/> + </description> +#ifdef MOZ_DATA_REPORTING + <hbox id="telemetry-container" class="info-box-container smaller-font-size" hidden="true"> + <hbox class="info-icon-container"> + <html:img class="info-icon"></html:img> + </hbox> + <description> + <html:span id="telemetryDisabledDescription" class="tail-with-learn-more" + data-l10n-id="collection-health-report-telemetry-disabled"/> + <html:a is="moz-support-link" + id="telemetryDataDeletionLearnMore" + class="learnMore" + data-l10n-id="collection-health-report-telemetry-disabled-link"/> + </description> + </hbox> + <vbox data-subcategory="reports"> + <description flex="1"> + <checkbox id="submitHealthReportBox" + data-l10n-id="collection-health-report" + class="tail-with-learn-more"/> + <html:a id="FHRLearnMore" + class="learnMore" + data-l10n-id="collection-health-report-link"/> + <vbox class="indent"> + <hbox align="center"> + <checkbox id="addonRecommendationEnabled" + class="tail-with-learn-more" + data-l10n-id="addon-recommendations"/> + <html:a is="moz-support-link" + id="addonRecommendationLearnMore" + class="learnMore" + data-l10n-id="addon-recommendations-link" + support-page="personalized-addons" + /> + </hbox> + </vbox> + </description> +#ifndef MOZ_TELEMETRY_REPORTING + <description id="TelemetryDisabledDesc" + class="indent tip-caption" control="telemetryGroup" + data-l10n-id="collection-health-report-disabled"/> +#endif + +#ifdef MOZ_NORMANDY + <hbox align="center"> + <checkbox id="optOutStudiesEnabled" + class="tail-with-learn-more" + data-l10n-id="collection-studies"/> + <html:a id="viewShieldStudies" + href="about:studies" + useoriginprincipal="true" + class="learnMore" + data-l10n-id="collection-studies-link"/> + </hbox> +#endif + +#ifdef MOZ_CRASHREPORTER + <hbox align="center" class="checkbox-row"> + <html:input type="checkbox" + id="automaticallySubmitCrashesBox" + preference="browser.crashReports.unsubmittedCheck.autoSubmit2"/> + <label for="automaticallySubmitCrashesBox" + id="crashReporterLabel" + data-l10n-id="collection-backlogged-crash-reports-with-link"> + <html:a data-l10n-name="crash-reports-link" id="crashReporterLearnMore" target="_blank"/> + </label> + </hbox> +#endif + </vbox> +#endif + <vbox id="privacySegmentationSection" data-subcategory="privacy-segmentation" hidden="true"> + <label> + <html:h2 data-l10n-id="privacy-segmentation-section-header"/> + </label> + <label data-l10n-id="privacy-segmentation-section-description"/> + <radiogroup id="privacyDataFeatureRecommendationRadioGroup" preference="browser.dataFeatureRecommendations.enabled"> + <radio id="privacyDataFeatureRecommendationEnabled" + data-l10n-id="privacy-segmentation-radio-off" + value="true"/> + <radio id="privacyDataFeatureRecommendationDisabled" + data-l10n-id="privacy-segmentation-radio-on" + value="false"/> + </radiogroup> + </vbox> +</groupbox> + +<hbox id="securityCategory" + class="subcategory" + hidden="true" + data-category="panePrivacy"> + <html:h1 data-l10n-id="security-header"/> +</hbox> + +<!-- addons, forgery (phishing) UI Security --> +<groupbox id="browsingProtectionGroup" data-category="panePrivacy" hidden="true"> + <label><html:h2 data-l10n-id="security-browsing-protection"/></label> + <hbox align = "center"> + <checkbox id="enableSafeBrowsing" + data-l10n-id="security-enable-safe-browsing" + class="tail-with-learn-more"/> + <html:a is="moz-support-link" + id="enableSafeBrowsingLearnMore" + class="learnMore" + data-l10n-id="security-enable-safe-browsing-link" + support-page="phishing-malware" + /> + </hbox> + <vbox class="indent"> + <checkbox id="blockDownloads" + data-l10n-id="security-block-downloads"/> + <checkbox id="blockUncommonUnwanted" + data-l10n-id="security-block-uncommon-software"/> + </vbox> +</groupbox> + +<!-- Certificates --> +<groupbox id="certSelection" data-category="panePrivacy" hidden="true"> + <label><html:h2 data-l10n-id="certs-header"/></label> + <hbox align="start"> + <checkbox id="enableOCSP" + data-l10n-id="certs-enable-ocsp" + preference="security.OCSP.enabled" + flex="1" /> + <vbox align="end"> + <button id="viewCertificatesButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="certs-view" + preference="security.disable_button.openCertManager" + search-l10n-ids=" + certmgr-tab-mine.label, + certmgr-tab-people.label, + certmgr-tab-servers.label, + certmgr-tab-ca.label, + certmgr-mine, + certmgr-people, + certmgr-server, + certmgr-ca, + certmgr-cert-name.label, + certmgr-token-name.label, + certmgr-view.label, + certmgr-export.label, + certmgr-delete.label + "/> + <button id="viewSecurityDevicesButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="certs-devices" + preference="security.disable_button.openDeviceManager" + search-l10n-ids=" + devmgr-window.title, + devmgr-devlist.label, + devmgr-header-details.label, + devmgr-header-value.label, + devmgr-button-login.label, + devmgr-button-logout.label, + devmgr-button-changepw.label, + devmgr-button-load.label, + devmgr-button-unload.label + "/> + </vbox> + </hbox> +</groupbox> + +<!-- HTTPS-ONLY Mode --> +<groupbox id="httpsOnlyBox" data-category="panePrivacy" hidden="true"> + <label><html:h2 data-l10n-id="httpsonly-header"/></label> + <vbox data-subcategory="httpsonly" flex="1"> + <label id="httpsOnlyDescription" data-l10n-id="httpsonly-description"/> + <html:a is="moz-support-link" + id="httpsOnlyLearnMore" + class="learnMore" + data-l10n-id="httpsonly-learn-more" + support-page="https-only-prefs" + /> + </vbox> + <hbox> + <radiogroup flex="1" id="httpsOnlyRadioGroup"> + <radio id="httpsOnlyRadioEnabled" + data-l10n-id="httpsonly-radio-enabled" + value="enabled"/> + <radio id="httpsOnlyRadioEnabledPBM" + data-l10n-id="httpsonly-radio-enabled-pbm" + value="privateOnly"/> + <radio id="httpsOnlyRadioDisabled" + data-l10n-id="httpsonly-radio-disabled" + value="disabled"/> + </radiogroup> + <vbox> + <button id="httpsOnlyExceptionButton" is="highlightable-button" class="accessory-button" disabled="true" + data-l10n-id="sitedata-cookies-exceptions" search-l10n-ids=" + permissions-address, + permissions-allow.label, + permissions-remove.label, + permissions-remove-all.label, + permissions-exceptions-https-only-desc, + " /> + </vbox> + </hbox> +</groupbox> + +<!-- DoH --> +<hbox id="DoHCategory" + class="subcategory" + hidden="true" + data-category="panePrivacy"> + <html:h1 data-l10n-id="preferences-doh-header"/> +</hbox> + +<groupbox id="dohBox" data-category="panePrivacy" data-subcategory="doh" hidden="true" class="highlighting-group"> + <label class="search-header" searchkeywords="doh trr" hidden="true"><html:h2 data-l10n-id="preferences-doh-header"/></label> + <vbox flex="1"> + <html:span id="dohDescription" class="tail-with-learn-more" data-l10n-id="preferences-doh-description"></html:span> + <html:a is="moz-support-link" + id="dohLearnMore" + class="learnMore" + support-page="dns-over-https" + /> + </vbox> + <vbox id="dohStatusSection" class="privacy-detailedoption info-box-container"> + <hbox> + <label id="dohStatus" class="doh-status-label tail-with-learn-more"/> + <html:a is="moz-support-link" + id="dohStatusLearnMore" + class="learnMore" + support-page="doh-status" + hidden="true"/> + </hbox> + <label class="doh-status-label" id="dohResolver"/> + <label class="doh-status-label" id="dohSteeringStatus" data-l10n-id="preferences-doh-steering-status" hidden="true"/> + </vbox> + <hbox id="dohExceptionBox"> + <label flex="1" data-l10n-id="preferences-doh-exceptions-description"/> + <button id="dohExceptionsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="preferences-doh-manage-exceptions" + search-l10n-ids=" + permissions-doh-entry-field, + permissions-doh-add-exception.label, + permissions-doh-remove.label, + permissions-doh-remove-all.label, + permissions-exceptions-doh-window.title, + permissions-exceptions-manage-doh-desc, + "/> + </hbox> + <vbox> + <label><html:h2 id="dohGroupMessage" data-l10n-id="preferences-doh-group-message"/></label> + <vbox id="dohCategories"> + <radiogroup id="dohCategoryRadioGroup" + preference="network.trr.mode" aria-labelledby="dohGroupMessage"> + <vbox id="dohOptionDefault" class="privacy-detailedoption info-box-container"> + <hbox> + <radio id="dohDefaultRadio" + value="0" + data-l10n-id="preferences-doh-setting-default" + flex="1"/> + <button id="dohDefaultArrow" + is="highlightable-button" + class="arrowhead doh-expand-section" + data-l10n-id="preferences-doh-expand-section" + aria-expanded="false"/> + </hbox> + <vbox class="indent"> + <label data-l10n-id="preferences-doh-default-desc"></label> + <vbox class="privacy-extra-information"> + <vbox class="indent"> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-default-detailed-desc-1"/> + </hbox> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-default-detailed-desc-2"/> + </hbox> + <hbox class="extra-information-label"> + <label class="doh-label tail-with-learn-more" data-l10n-id="preferences-doh-default-detailed-desc-3"/> + <html:a is="moz-support-link" + id="default-desc-3-learn-more" + class="learnMore" + support-page="doh-local-provider" + /> + </hbox> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-default-detailed-desc-4"/> + </hbox> + <hbox class="extra-information-label"> + <label class="doh-label tail-with-learn-more" data-l10n-id="preferences-doh-default-detailed-desc-5"/> + <html:a is="moz-support-link" + id="default-desc-5-learn-more" + class="learnMore" + support-page="firefox-turn-off-secure-dns" + /> + </hbox> + </vbox> + <hbox id="dohWarningBox1" class="extra-information-label" hidden="true"> + <checkbox id="dohWarnCheckbox1" + flex="1" + data-l10n-id="preferences-doh-checkbox-warn" + preference="network.trr.display_fallback_warning"/> + </hbox> + </vbox> + </vbox> + </vbox> + <vbox id="dohOptionEnabled" class="privacy-detailedoption info-box-container"> + <hbox> + <radio id="dohEnabledRadio" + value="2" + data-l10n-id="preferences-doh-setting-enabled" + flex="1"/> + <button id="dohEnabledArrow" + is="highlightable-button" + class="arrowhead doh-expand-section" + data-l10n-id="preferences-doh-expand-section" + aria-expanded="false"/> + </hbox> + <vbox class="indent"> + <label data-l10n-id="preferences-doh-enabled-desc"></label> + <vbox class="privacy-extra-information"> + <vbox class="indent"> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-enabled-detailed-desc-1"/> + </hbox> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-enabled-detailed-desc-2"/> + </hbox> + </vbox> + <hbox id="dohWarningBox2" class="extra-information-label" hidden="true"> + <checkbox id="dohWarnCheckbox2" + flex="1" + data-l10n-id="preferences-doh-checkbox-warn" + preference="network.trr.display_fallback_warning"/> + </hbox> + <vbox class="extra-information-label"> + <label data-l10n-id="preferences-doh-select-resolver"/> + <menulist id="dohEnabledResolverChoices" + sizetopopup="none"> + </menulist> + <html:input id="dohEnabledInputField" type="text" style="flex: 1;" + preference="network.trr.custom_uri" hidden="true"/> + </vbox> + </vbox> + </vbox> + </vbox> + <vbox id="dohOptionStrict" class="privacy-detailedoption info-box-container"> + <hbox> + <radio id="dohStrictRadio" + value="3" + data-l10n-id="preferences-doh-setting-strict" + flex="1"/> + <button id="dohStrictArrow" + is="highlightable-button" + class="arrowhead doh-expand-section" + data-l10n-id="preferences-doh-expand-section" + aria-expanded="false"/> + </hbox> + <vbox class="indent"> + <label data-l10n-id="preferences-doh-strict-desc"></label> + <vbox class="privacy-extra-information"> + <vbox class="indent"> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-strict-detailed-desc-1"/> + </hbox> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-strict-detailed-desc-2"/> + </hbox> + <hbox class="extra-information-label"> + <label class="doh-label" data-l10n-id="preferences-doh-strict-detailed-desc-3"/> + </hbox> + </vbox> + <vbox class="extra-information-label"> + <label data-l10n-id="preferences-doh-select-resolver"/> + <menulist id="dohStrictResolverChoices" + sizetopopup="none"> + </menulist> + <html:input id="dohStrictInputField" type="text" style="flex: 1;" + preference="network.trr.custom_uri" hidden="true"/> + </vbox> + </vbox> + </vbox> + </vbox> + <vbox id="dohOptionOff" class="privacy-detailedoption info-box-container"> + <hbox> + <radio id="dohOffRadio" + value="5" + data-l10n-id="preferences-doh-setting-off" + flex="1"/> + </hbox> + <vbox class="indent"> + <label data-l10n-id="preferences-doh-off-desc"></label> + </vbox> + </vbox> + </radiogroup> + </vbox> + </vbox> +</groupbox> + +</html:template> diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js new file mode 100644 index 0000000000..c6784ea84a --- /dev/null +++ b/browser/components/preferences/privacy.js @@ -0,0 +1,3358 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from extensionControlled.js */ +/* import-globals-from preferences.js */ + +const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +const TRACKING_PROTECTION_KEY = "websites.trackingProtectionMode"; +const TRACKING_PROTECTION_PREFS = [ + "privacy.trackingprotection.enabled", + "privacy.trackingprotection.pbmode.enabled", +]; +const CONTENT_BLOCKING_PREFS = [ + "privacy.trackingprotection.enabled", + "privacy.trackingprotection.pbmode.enabled", + "network.cookie.cookieBehavior", + "privacy.trackingprotection.fingerprinting.enabled", + "privacy.trackingprotection.cryptomining.enabled", + "privacy.firstparty.isolate", + "privacy.trackingprotection.emailtracking.enabled", + "privacy.trackingprotection.emailtracking.pbmode.enabled", +]; + +const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST = + "browser.urlbar.quicksuggest.blockedDigests"; +const PREF_URLBAR_WEATHER_USER_ENABLED = "browser.urlbar.suggest.weather"; + +/* + * Prefs that are unique to sanitizeOnShutdown and are not shared + * with the deleteOnClose mechanism like privacy.clearOnShutdown.cookies, -cache and -offlineApps + */ +const SANITIZE_ON_SHUTDOWN_PREFS_ONLY = [ + "privacy.clearOnShutdown.history", + "privacy.clearOnShutdown.downloads", + "privacy.clearOnShutdown.sessions", + "privacy.clearOnShutdown.formdata", + "privacy.clearOnShutdown.siteSettings", +]; + +const PREF_OPT_OUT_STUDIES_ENABLED = "app.shield.optoutstudies.enabled"; +const PREF_NORMANDY_ENABLED = "app.normandy.enabled"; + +const PREF_ADDON_RECOMMENDATIONS_ENABLED = "browser.discovery.enabled"; + +const PREF_PASSWORD_GENERATION_AVAILABLE = "signon.generation.available"; +const { BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN } = Ci.nsICookieService; + +const PASSWORD_MANAGER_PREF_ID = "services.passwordSavingEnabled"; + +XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function () { + try { + let alertsService = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIAlertsDoNotDisturb); + // This will throw if manualDoNotDisturb isn't implemented. + alertsService.manualDoNotDisturb; + return alertsService; + } catch (ex) { + return undefined; + } +}); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "OS_AUTH_ENABLED", + "signon.management.page.os-auth.enabled", + true +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gIsFirstPartyIsolated", + "privacy.firstparty.isolate", + false +); + +ChromeUtils.defineESModuleGetters(this, { + DoHConfigController: "resource:///modules/DoHConfig.sys.mjs", +}); + +Preferences.addAll([ + // Content blocking / Tracking Protection + { id: "privacy.trackingprotection.enabled", type: "bool" }, + { id: "privacy.trackingprotection.pbmode.enabled", type: "bool" }, + { id: "privacy.trackingprotection.fingerprinting.enabled", type: "bool" }, + { id: "privacy.trackingprotection.cryptomining.enabled", type: "bool" }, + { id: "privacy.trackingprotection.emailtracking.enabled", type: "bool" }, + { + id: "privacy.trackingprotection.emailtracking.pbmode.enabled", + type: "bool", + }, + + // Social tracking + { id: "privacy.trackingprotection.socialtracking.enabled", type: "bool" }, + { id: "privacy.socialtracking.block_cookies.enabled", type: "bool" }, + + // Tracker list + { id: "urlclassifier.trackingTable", type: "string" }, + + // Button prefs + { id: "pref.privacy.disable_button.cookie_exceptions", type: "bool" }, + { id: "pref.privacy.disable_button.view_cookies", type: "bool" }, + { id: "pref.privacy.disable_button.change_blocklist", type: "bool" }, + { + id: "pref.privacy.disable_button.tracking_protection_exceptions", + type: "bool", + }, + + // Location Bar + { id: "browser.urlbar.suggest.bestmatch", type: "bool" }, + { id: "browser.urlbar.suggest.bookmark", type: "bool" }, + { id: "browser.urlbar.suggest.history", type: "bool" }, + { id: "browser.urlbar.suggest.openpage", type: "bool" }, + { id: "browser.urlbar.suggest.topsites", type: "bool" }, + { id: "browser.urlbar.suggest.engines", type: "bool" }, + { id: "browser.urlbar.suggest.quicksuggest.nonsponsored", type: "bool" }, + { id: "browser.urlbar.suggest.quicksuggest.sponsored", type: "bool" }, + { id: "browser.urlbar.quicksuggest.dataCollection.enabled", type: "bool" }, + { id: PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, type: "string" }, + { id: PREF_URLBAR_WEATHER_USER_ENABLED, type: "bool" }, + + // History + { id: "places.history.enabled", type: "bool" }, + { id: "browser.formfill.enable", type: "bool" }, + { id: "privacy.history.custom", type: "bool" }, + + // Cookies + { id: "network.cookie.cookieBehavior", type: "int" }, + { id: "network.cookie.blockFutureCookies", type: "bool" }, + // Content blocking category + { id: "browser.contentblocking.category", type: "string" }, + { id: "browser.contentblocking.features.strict", type: "string" }, + + // Clear Private Data + { id: "privacy.sanitize.sanitizeOnShutdown", type: "bool" }, + { id: "privacy.sanitize.timeSpan", type: "int" }, + { id: "privacy.clearOnShutdown.cookies", type: "bool" }, + { id: "privacy.clearOnShutdown.cache", type: "bool" }, + { id: "privacy.clearOnShutdown.offlineApps", type: "bool" }, + { id: "privacy.clearOnShutdown.history", type: "bool" }, + { id: "privacy.clearOnShutdown.downloads", type: "bool" }, + { id: "privacy.clearOnShutdown.sessions", type: "bool" }, + { id: "privacy.clearOnShutdown.formdata", type: "bool" }, + { id: "privacy.clearOnShutdown.siteSettings", type: "bool" }, + + // Do not track + { id: "privacy.donottrackheader.enabled", type: "bool" }, + + // Media + { id: "media.autoplay.default", type: "int" }, + + // Popups + { id: "dom.disable_open_during_load", type: "bool" }, + // Passwords + { id: "signon.rememberSignons", type: "bool" }, + { id: "signon.generation.enabled", type: "bool" }, + { id: "signon.autofillForms", type: "bool" }, + { id: "signon.management.page.breach-alerts.enabled", type: "bool" }, + { id: "signon.firefoxRelay.feature", type: "string" }, + + // Buttons + { id: "pref.privacy.disable_button.view_passwords", type: "bool" }, + { id: "pref.privacy.disable_button.view_passwords_exceptions", type: "bool" }, + + /* Certificates tab + * security.default_personal_cert + * - a string: + * "Select Automatically" select a certificate automatically when a site + * requests one + * "Ask Every Time" present a dialog to the user so he can select + * the certificate to use on a site which + * requests one + */ + { id: "security.default_personal_cert", type: "string" }, + + { id: "security.disable_button.openCertManager", type: "bool" }, + + { id: "security.disable_button.openDeviceManager", type: "bool" }, + + { id: "security.OCSP.enabled", type: "int" }, + + // Add-ons, malware, phishing + { id: "xpinstall.whitelist.required", type: "bool" }, + + { id: "browser.safebrowsing.malware.enabled", type: "bool" }, + { id: "browser.safebrowsing.phishing.enabled", type: "bool" }, + + { id: "browser.safebrowsing.downloads.enabled", type: "bool" }, + + { id: "urlclassifier.malwareTable", type: "string" }, + + { + id: "browser.safebrowsing.downloads.remote.block_potentially_unwanted", + type: "bool", + }, + { id: "browser.safebrowsing.downloads.remote.block_uncommon", type: "bool" }, + + // First-Party Isolation + { id: "privacy.firstparty.isolate", type: "bool" }, + + // HTTPS-Only + { id: "dom.security.https_only_mode", type: "bool" }, + { id: "dom.security.https_only_mode_pbm", type: "bool" }, + + // Windows SSO + { id: "network.http.windows-sso.enabled", type: "bool" }, + + // Quick Actions + { id: "browser.urlbar.quickactions.showPrefs", type: "bool" }, + { id: "browser.urlbar.suggest.quickactions", type: "bool" }, + + // Cookie Banner Handling + { id: "cookiebanners.ui.desktop.enabled", type: "bool" }, + { id: "cookiebanners.service.mode", type: "int" }, + { id: "cookiebanners.service.detectOnly", type: "bool" }, + + // DoH + { id: "network.trr.mode", type: "int" }, + { id: "network.trr.uri", type: "string" }, + { id: "network.trr.default_provider_uri", type: "string" }, + { id: "network.trr.custom_uri", type: "string" }, + { id: "network.trr_ui.show_fallback_warning_option", type: "bool" }, + { id: "network.trr.display_fallback_warning", type: "bool" }, + { id: "doh-rollout.disable-heuristics", type: "bool" }, +]); + +// Study opt out +if (AppConstants.MOZ_DATA_REPORTING) { + Preferences.addAll([ + // Preference instances for prefs that we need to monitor while the page is open. + { id: PREF_OPT_OUT_STUDIES_ENABLED, type: "bool" }, + { id: PREF_ADDON_RECOMMENDATIONS_ENABLED, type: "bool" }, + { id: PREF_UPLOAD_ENABLED, type: "bool" }, + ]); +} +// Privacy segmentation section +Preferences.add({ + id: "browser.dataFeatureRecommendations.enabled", + type: "bool", +}); + +// Data Choices tab +if (AppConstants.MOZ_CRASHREPORTER) { + Preferences.add({ + id: "browser.crashReports.unsubmittedCheck.autoSubmit2", + type: "bool", + }); +} + +function setEventListener(aId, aEventType, aCallback) { + document + .getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gPrivacyPane)); +} + +function setSyncFromPrefListener(aId, aCallback) { + Preferences.addSyncFromPrefListener(document.getElementById(aId), aCallback); +} + +function setSyncToPrefListener(aId, aCallback) { + Preferences.addSyncToPrefListener(document.getElementById(aId), aCallback); +} + +function dataCollectionCheckboxHandler({ + checkbox, + pref, + matchPref = () => true, + isDisabled = () => false, +}) { + function updateCheckbox() { + let collectionEnabled = Services.prefs.getBoolPref( + PREF_UPLOAD_ENABLED, + false + ); + + if (collectionEnabled && matchPref()) { + if (Services.prefs.getBoolPref(pref, false)) { + checkbox.setAttribute("checked", "true"); + } else { + checkbox.removeAttribute("checked"); + } + checkbox.setAttribute("preference", pref); + } else { + checkbox.removeAttribute("preference"); + checkbox.removeAttribute("checked"); + } + + checkbox.disabled = + !collectionEnabled || Services.prefs.prefIsLocked(pref) || isDisabled(); + } + + Preferences.get(PREF_UPLOAD_ENABLED).on("change", updateCheckbox); + updateCheckbox(); +} + +// Sets the "Learn how" SUMO link in the Strict/Custom options of Content Blocking. +function setUpContentBlockingWarnings() { + document.getElementById("fpiIncompatibilityWarning").hidden = + !gIsFirstPartyIsolated; +} + +function initTCPStandardSection() { + let cookieBehaviorPref = Preferences.get("network.cookie.cookieBehavior"); + let updateTCPSectionVisibilityState = () => { + document.getElementById("etpStandardTCPBox").hidden = + cookieBehaviorPref.value != + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + }; + + cookieBehaviorPref.on("change", updateTCPSectionVisibilityState); + + updateTCPSectionVisibilityState(); +} + +var gPrivacyPane = { + _pane: null, + + /** + * Whether the prompt to restart Firefox should appear when changing the autostart pref. + */ + _shouldPromptForRestart: true, + + /** + * Update the tracking protection UI to deal with extension control. + */ + _updateTrackingProtectionUI() { + let cBPrefisLocked = CONTENT_BLOCKING_PREFS.some(pref => + Services.prefs.prefIsLocked(pref) + ); + let tPPrefisLocked = TRACKING_PROTECTION_PREFS.some(pref => + Services.prefs.prefIsLocked(pref) + ); + + function setInputsDisabledState(isControlled) { + let tpDisabled = tPPrefisLocked || isControlled; + let disabled = cBPrefisLocked || isControlled; + let tpCheckbox = document.getElementById( + "contentBlockingTrackingProtectionCheckbox" + ); + // Only enable the TP menu if Detect All Trackers is enabled. + document.getElementById("trackingProtectionMenu").disabled = + tpDisabled || !tpCheckbox.checked; + tpCheckbox.disabled = tpDisabled; + + document.getElementById("standardRadio").disabled = disabled; + document.getElementById("strictRadio").disabled = disabled; + document + .getElementById("contentBlockingOptionStrict") + .classList.toggle("disabled", disabled); + document + .getElementById("contentBlockingOptionStandard") + .classList.toggle("disabled", disabled); + let arrowButtons = document.querySelectorAll("button.arrowhead"); + for (let button of arrowButtons) { + button.disabled = disabled; + } + + // Notify observers that the TP UI has been updated. + // This is needed since our tests need to be notified about the + // trackingProtectionMenu element getting disabled/enabled at the right time. + Services.obs.notifyObservers(window, "privacy-pane-tp-ui-updated"); + } + + let policy = Services.policies.getActivePolicies(); + if ( + policy && + ((policy.EnableTrackingProtection && + policy.EnableTrackingProtection.Locked) || + (policy.Cookies && policy.Cookies.Locked)) + ) { + setInputsDisabledState(true); + } + if (tPPrefisLocked) { + // An extension can't control this setting if either pref is locked. + hideControllingExtension(TRACKING_PROTECTION_KEY); + setInputsDisabledState(false); + } else { + handleControllingExtension( + PREF_SETTING_TYPE, + TRACKING_PROTECTION_KEY + ).then(setInputsDisabledState); + } + }, + + /** + * Hide the "Change Block List" link for trackers/tracking content in the + * custom Content Blocking/ETP panel. By default, it will not be visible. + */ + _showCustomBlockList() { + let prefValue = Services.prefs.getBoolPref( + "browser.contentblocking.customBlockList.preferences.ui.enabled" + ); + if (!prefValue) { + document.getElementById("changeBlockListLink").style.display = "none"; + } else { + setEventListener("changeBlockListLink", "click", this.showBlockLists); + } + }, + + /** + * Set up handlers for showing and hiding controlling extension info + * for tracking protection. + */ + _initTrackingProtectionExtensionControl() { + setEventListener( + "contentBlockingDisableTrackingProtectionExtension", + "command", + makeDisableControllingExtension( + PREF_SETTING_TYPE, + TRACKING_PROTECTION_KEY + ) + ); + + let trackingProtectionObserver = { + observe(subject, topic, data) { + gPrivacyPane._updateTrackingProtectionUI(); + }, + }; + + for (let pref of TRACKING_PROTECTION_PREFS) { + Services.prefs.addObserver(pref, trackingProtectionObserver); + } + window.addEventListener("unload", () => { + for (let pref of TRACKING_PROTECTION_PREFS) { + Services.prefs.removeObserver(pref, trackingProtectionObserver); + } + }); + }, + + _initQuickActionsSection() { + let showPref = Preferences.get("browser.urlbar.quickactions.showPrefs"); + let showQuickActionsGroup = () => { + document.getElementById("quickActionsBox").hidden = !showPref.value; + }; + showPref.on("change", showQuickActionsGroup); + showQuickActionsGroup(); + }, + + syncFromHttpsOnlyPref() { + let httpsOnlyOnPref = Services.prefs.getBoolPref( + "dom.security.https_only_mode" + ); + let httpsOnlyOnPBMPref = Services.prefs.getBoolPref( + "dom.security.https_only_mode_pbm" + ); + let httpsOnlyRadioGroup = document.getElementById("httpsOnlyRadioGroup"); + let httpsOnlyExceptionButton = document.getElementById( + "httpsOnlyExceptionButton" + ); + + if (httpsOnlyOnPref) { + httpsOnlyRadioGroup.value = "enabled"; + httpsOnlyExceptionButton.disabled = false; + } else if (httpsOnlyOnPBMPref) { + httpsOnlyRadioGroup.value = "privateOnly"; + httpsOnlyExceptionButton.disabled = true; + } else { + httpsOnlyRadioGroup.value = "disabled"; + httpsOnlyExceptionButton.disabled = true; + } + + if ( + Services.prefs.prefIsLocked("dom.security.https_only_mode") || + Services.prefs.prefIsLocked("dom.security.https_only_mode_pbm") + ) { + httpsOnlyRadioGroup.disabled = true; + } + }, + + syncToHttpsOnlyPref() { + let value = document.getElementById("httpsOnlyRadioGroup").value; + Services.prefs.setBoolPref( + "dom.security.https_only_mode_pbm", + value == "privateOnly" + ); + Services.prefs.setBoolPref( + "dom.security.https_only_mode", + value == "enabled" + ); + }, + + /** + * Init HTTPS-Only mode and corresponding prefs + */ + initHttpsOnly() { + // Set radio-value based on the pref value + this.syncFromHttpsOnlyPref(); + + // Create event listener for when the user clicks + // on one of the radio buttons + setEventListener( + "httpsOnlyRadioGroup", + "command", + this.syncToHttpsOnlyPref + ); + // Update radio-value when the pref changes + Preferences.get("dom.security.https_only_mode").on("change", () => + this.syncFromHttpsOnlyPref() + ); + Preferences.get("dom.security.https_only_mode_pbm").on("change", () => + this.syncFromHttpsOnlyPref() + ); + }, + + 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; + }, + + updateDoHResolverList(mode) { + let resolvers = this.dnsOverHttpsResolvers; + let currentURI = Preferences.get("network.trr.uri").value; + if (!currentURI) { + currentURI = Preferences.get("network.trr.default_provider_uri").value; + } + let menu = document.getElementById(`${mode}ResolverChoices`); + + 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; + + let customInput = document.getElementById(`${mode}InputField`); + customInput.hidden = menu.value != "custom"; + }, + + populateDoHResolverList(mode) { + let resolvers = this.dnsOverHttpsResolvers; + let defaultURI = DoHConfigController.currentConfig.fallbackProviderURI; + let menu = document.getElementById(`${mode}ResolverChoices`); + + // 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 + this.updateDoHResolverList(mode); + + let customInput = document.getElementById(`${mode}InputField`); + + function updateURIPref() { + if (customInput.value == "") { + // Setting the pref to empty string will make it have the default + // pref value which makes us fallback to using the default TRR + // resolver in network.trr.default_provider_uri. + // If the input is empty we set it to "(space)" which is essentially + // the same. + Services.prefs.setStringPref("network.trr.uri", " "); + } else { + Services.prefs.setStringPref("network.trr.uri", customInput.value); + } + } + + menu.addEventListener("command", () => { + if (menu.value == "custom") { + customInput.hidden = false; + updateURIPref(); + } else { + customInput.hidden = true; + Services.prefs.setStringPref("network.trr.uri", menu.value); + } + Services.telemetry.recordEvent( + "security.doh.settings", + "provider_choice", + "value", + menu.value + ); + + // Update other menu too. + let otherMode = mode == "dohEnabled" ? "dohStrict" : "dohEnabled"; + let otherMenu = document.getElementById(`${otherMode}ResolverChoices`); + let otherInput = document.getElementById(`${otherMode}InputField`); + otherMenu.value = menu.value; + otherInput.hidden = otherMenu.value != "custom"; + }); + + // Change the URL when you press ENTER in the input field it or loses focus + customInput.addEventListener("change", () => { + updateURIPref(); + }); + }, + + async updateDoHStatus() { + let trrURI = Services.dns.currentTrrURI; + let hostname = ""; + try { + hostname = new URL(trrURI).hostname; + } catch (e) { + hostname = await document.l10n.formatValue("preferences-doh-bad-url"); + } + + let steering = document.getElementById("dohSteeringStatus"); + steering.hidden = true; + + let dohResolver = document.getElementById("dohResolver"); + dohResolver.hidden = true; + + let status = document.getElementById("dohStatus"); + + async function setStatus(localizedStringName, options) { + let opts = options || {}; + let statusString = await document.l10n.formatValue( + localizedStringName, + opts + ); + document.l10n.setAttributes(status, "preferences-doh-status", { + status: statusString, + }); + } + + function computeStatus() { + let mode = Services.dns.currentTrrMode; + let confirmationState = Services.dns.currentTrrConfirmationState; + if ( + mode == Ci.nsIDNSService.MODE_TRRFIRST || + mode == Ci.nsIDNSService.MODE_TRRONLY + ) { + switch (confirmationState) { + case Ci.nsIDNSService.CONFIRM_TRYING_OK: + case Ci.nsIDNSService.CONFIRM_OK: + case Ci.nsIDNSService.CONFIRM_DISABLED: + return "preferences-doh-status-active"; + default: + return "preferences-doh-status-not-active"; + } + } + + return "preferences-doh-status-disabled"; + } + + let errReason = ""; + let confirmationStatus = Services.dns.lastConfirmationStatus; + if (confirmationStatus != Cr.NS_OK) { + errReason = ChromeUtils.getXPCOMErrorName(confirmationStatus); + } else { + errReason = Services.dns.getTRRSkipReasonName( + Services.dns.lastConfirmationSkipReason + ); + } + let statusLabel = computeStatus(); + // setStatus will format and set the statusLabel asynchronously. + setStatus(statusLabel, { reason: errReason }); + dohResolver.hidden = statusLabel == "preferences-doh-status-disabled"; + + let statusLearnMore = document.getElementById("dohStatusLearnMore"); + statusLearnMore.hidden = statusLabel != "preferences-doh-status-not-active"; + + // No need to set the resolver name since we're not going to show it. + if (statusLabel == "preferences-doh-status-disabled") { + return; + } + + function nameOrDomain() { + for (let resolver of DoHConfigController.currentConfig.providerList) { + if (resolver.uri == trrURI) { + return resolver.UIName || hostname || trrURI; + } + } + + // Also check if this is a steering provider. + for (let resolver of DoHConfigController.currentConfig.providerSteering + .providerList) { + if (resolver.uri == trrURI) { + steering.hidden = false; + return resolver.UIName || hostname || trrURI; + } + } + + return hostname; + } + + let resolverNameOrDomain = nameOrDomain(); + document.l10n.setAttributes(dohResolver, "preferences-doh-resolver", { + name: resolverNameOrDomain, + }); + }, + + highlightDoHCategoryAndUpdateStatus() { + let value = Preferences.get("network.trr.mode").value; + let defaultOption = document.getElementById("dohOptionDefault"); + let enabledOption = document.getElementById("dohOptionEnabled"); + let strictOption = document.getElementById("dohOptionStrict"); + let offOption = document.getElementById("dohOptionOff"); + defaultOption.classList.remove("selected"); + enabledOption.classList.remove("selected"); + strictOption.classList.remove("selected"); + offOption.classList.remove("selected"); + + switch (value) { + case Ci.nsIDNSService.MODE_NATIVEONLY: + defaultOption.classList.add("selected"); + break; + case Ci.nsIDNSService.MODE_TRRFIRST: + enabledOption.classList.add("selected"); + break; + case Ci.nsIDNSService.MODE_TRRONLY: + strictOption.classList.add("selected"); + break; + case Ci.nsIDNSService.MODE_TRROFF: + offOption.classList.add("selected"); + break; + default: + // The pref is set to a random value. + // This shouldn't happen, but let's make sure off is selected. + offOption.classList.add("selected"); + document.getElementById("dohCategoryRadioGroup").selectedIndex = 3; + break; + } + + // When the mode is set to 0 we need to clear the URI so + // doh-rollout can kick in. + if (value == Ci.nsIDNSService.MODE_NATIVEONLY) { + Services.prefs.clearUserPref("network.trr.uri"); + Services.prefs.clearUserPref("doh-rollout.disable-heuristics"); + } + + gPrivacyPane.updateDoHStatus(); + }, + + /** + * Init DoH corresponding prefs + */ + initDoH() { + Services.telemetry.setEventRecordingEnabled("security.doh.settings", true); + + setEventListener("dohDefaultArrow", "command", this.toggleExpansion); + setEventListener("dohEnabledArrow", "command", this.toggleExpansion); + setEventListener("dohStrictArrow", "command", this.toggleExpansion); + + function modeButtonPressed(e) { + // Clicking the active mode again should not generate another event + if ( + parseInt(e.target.value) == Preferences.get("network.trr.mode").value + ) { + return; + } + Services.telemetry.recordEvent( + "security.doh.settings", + "mode_changed", + "button", + e.target.id + ); + } + + setEventListener("dohDefaultRadio", "command", modeButtonPressed); + setEventListener("dohEnabledRadio", "command", modeButtonPressed); + setEventListener("dohStrictRadio", "command", modeButtonPressed); + setEventListener("dohOffRadio", "command", modeButtonPressed); + + function warnCheckboxClicked(e) { + Services.telemetry.recordEvent( + "security.doh.settings", + "warn_checkbox", + "checkbox", + `${e.target.checked}` + ); + } + + setEventListener("dohWarnCheckbox1", "command", warnCheckboxClicked); + setEventListener("dohWarnCheckbox2", "command", warnCheckboxClicked); + + this.populateDoHResolverList("dohEnabled"); + this.populateDoHResolverList("dohStrict"); + + Preferences.get("network.trr.uri").on("change", () => { + gPrivacyPane.updateDoHResolverList("dohEnabled"); + gPrivacyPane.updateDoHResolverList("dohStrict"); + gPrivacyPane.updateDoHStatus(); + }); + + // Update status box and hightlightling when the pref changes + Preferences.get("network.trr.mode").on( + "change", + gPrivacyPane.highlightDoHCategoryAndUpdateStatus + ); + this.highlightDoHCategoryAndUpdateStatus(); + + Services.obs.addObserver(this, "network:trr-uri-changed"); + Services.obs.addObserver(this, "network:trr-mode-changed"); + Services.obs.addObserver(this, "network:trr-confirmation"); + let unload = () => { + Services.obs.removeObserver(this, "network:trr-uri-changed"); + Services.obs.removeObserver(this, "network:trr-mode-changed"); + Services.obs.removeObserver(this, "network:trr-confirmation"); + }; + window.addEventListener("unload", unload, { once: true }); + + if (Preferences.get("network.trr_ui.show_fallback_warning_option").value) { + document.getElementById("dohWarningBox1").hidden = false; + document.getElementById("dohWarningBox2").hidden = false; + } + + let uriPref = Services.prefs.getStringPref("network.trr.uri"); + // If the value isn't one of the providers, we need to update the + // custom_uri pref to make sure the input box contains the correct URL. + if (uriPref && !this.dnsOverHttpsResolvers.some(e => e.uri == uriPref)) { + Services.prefs.setStringPref( + "network.trr.custom_uri", + Services.prefs.getStringPref("network.trr.uri") + ); + } + + if (Services.prefs.prefIsLocked("network.trr.mode")) { + document.getElementById("dohCategoryRadioGroup").disabled = true; + Services.prefs.setStringPref("network.trr.custom_uri", uriPref); + } + }, + + /** + * Sets up the UI for the number of days of history to keep, and updates the + * label of the "Clear Now..." button. + */ + init() { + this._updateSanitizeSettingsButton(); + this.initDeleteOnCloseBox(); + this.syncSanitizationPrefsWithDeleteOnClose(); + this.initializeHistoryMode(); + this.updateHistoryModePane(); + this.updatePrivacyMicroControls(); + this.initAutoStartPrivateBrowsingReverter(); + + /* Initialize Content Blocking */ + this.initContentBlocking(); + + this._showCustomBlockList(); + this.trackingProtectionReadPrefs(); + this.networkCookieBehaviorReadPrefs(); + this._initTrackingProtectionExtensionControl(); + + Services.telemetry.setEventRecordingEnabled("pwmgr", true); + + Preferences.get("privacy.trackingprotection.enabled").on( + "change", + gPrivacyPane.trackingProtectionReadPrefs.bind(gPrivacyPane) + ); + Preferences.get("privacy.trackingprotection.pbmode.enabled").on( + "change", + gPrivacyPane.trackingProtectionReadPrefs.bind(gPrivacyPane) + ); + + // Watch all of the prefs that the new Cookies & Site Data UI depends on + Preferences.get("network.cookie.cookieBehavior").on( + "change", + gPrivacyPane.networkCookieBehaviorReadPrefs.bind(gPrivacyPane) + ); + Preferences.get("browser.privatebrowsing.autostart").on( + "change", + gPrivacyPane.networkCookieBehaviorReadPrefs.bind(gPrivacyPane) + ); + Preferences.get("privacy.firstparty.isolate").on( + "change", + gPrivacyPane.networkCookieBehaviorReadPrefs.bind(gPrivacyPane) + ); + + setEventListener( + "trackingProtectionExceptions", + "command", + gPrivacyPane.showTrackingProtectionExceptions + ); + + Preferences.get("privacy.sanitize.sanitizeOnShutdown").on( + "change", + gPrivacyPane._updateSanitizeSettingsButton.bind(gPrivacyPane) + ); + Preferences.get("browser.privatebrowsing.autostart").on( + "change", + gPrivacyPane.updatePrivacyMicroControls.bind(gPrivacyPane) + ); + setEventListener("historyMode", "command", function () { + gPrivacyPane.updateHistoryModePane(); + gPrivacyPane.updateHistoryModePrefs(); + gPrivacyPane.updatePrivacyMicroControls(); + gPrivacyPane.updateAutostart(); + }); + setEventListener("clearHistoryButton", "command", function () { + let historyMode = document.getElementById("historyMode"); + // Select "everything" in the clear history dialog if the + // user has set their history mode to never remember history. + gPrivacyPane.clearPrivateDataNow(historyMode.value == "dontremember"); + }); + setEventListener("openSearchEnginePreferences", "click", function (event) { + if (event.button == 0) { + gotoPref("search"); + } + return false; + }); + setEventListener( + "privateBrowsingAutoStart", + "command", + gPrivacyPane.updateAutostart + ); + setEventListener( + "cookieExceptions", + "command", + gPrivacyPane.showCookieExceptions + ); + setEventListener( + "httpsOnlyExceptionButton", + "command", + gPrivacyPane.showHttpsOnlyModeExceptions + ); + setEventListener( + "dohExceptionsButton", + "command", + gPrivacyPane.showDoHExceptions + ); + setEventListener( + "clearDataSettings", + "command", + gPrivacyPane.showClearPrivateDataSettings + ); + setEventListener( + "passwordExceptions", + "command", + gPrivacyPane.showPasswordExceptions + ); + setEventListener( + "useMasterPassword", + "command", + gPrivacyPane.updateMasterPasswordButton + ); + setEventListener( + "changeMasterPassword", + "command", + gPrivacyPane.changeMasterPassword + ); + setEventListener("showPasswords", "command", gPrivacyPane.showPasswords); + setEventListener( + "addonExceptions", + "command", + gPrivacyPane.showAddonExceptions + ); + setEventListener( + "viewCertificatesButton", + "command", + gPrivacyPane.showCertificates + ); + setEventListener( + "viewSecurityDevicesButton", + "command", + gPrivacyPane.showSecurityDevices + ); + + this._pane = document.getElementById("panePrivacy"); + + this._initPasswordGenerationUI(); + this._initRelayIntegrationUI(); + this._initMasterPasswordUI(); + + this.initListenersForExtensionControllingPasswordManager(); + + this._initSafeBrowsing(); + + setEventListener( + "autoplaySettingsButton", + "command", + gPrivacyPane.showAutoplayMediaExceptions + ); + setEventListener( + "notificationSettingsButton", + "command", + gPrivacyPane.showNotificationExceptions + ); + setEventListener( + "locationSettingsButton", + "command", + gPrivacyPane.showLocationExceptions + ); + setEventListener( + "xrSettingsButton", + "command", + gPrivacyPane.showXRExceptions + ); + setEventListener( + "cameraSettingsButton", + "command", + gPrivacyPane.showCameraExceptions + ); + setEventListener( + "microphoneSettingsButton", + "command", + gPrivacyPane.showMicrophoneExceptions + ); + document.getElementById("speakerSettingsRow").hidden = + !Services.prefs.getBoolPref("media.setsinkid.enabled", false); + setEventListener( + "speakerSettingsButton", + "command", + gPrivacyPane.showSpeakerExceptions + ); + setEventListener( + "popupPolicyButton", + "command", + gPrivacyPane.showPopupExceptions + ); + setEventListener( + "notificationsDoNotDisturb", + "command", + gPrivacyPane.toggleDoNotDisturbNotifications + ); + + setSyncFromPrefListener("contentBlockingBlockCookiesCheckbox", () => + this.readBlockCookies() + ); + setSyncToPrefListener("contentBlockingBlockCookiesCheckbox", () => + this.writeBlockCookies() + ); + setSyncFromPrefListener("blockCookiesMenu", () => + this.readBlockCookiesFrom() + ); + setSyncToPrefListener("blockCookiesMenu", () => + this.writeBlockCookiesFrom() + ); + + setSyncFromPrefListener("savePasswords", () => this.readSavePasswords()); + + let microControlHandler = el => + this.ensurePrivacyMicroControlUncheckedWhenDisabled(el); + setSyncFromPrefListener("rememberHistory", microControlHandler); + setSyncFromPrefListener("rememberForms", microControlHandler); + setSyncFromPrefListener("alwaysClear", microControlHandler); + + setSyncFromPrefListener("popupPolicy", () => + this.updateButtons("popupPolicyButton", "dom.disable_open_during_load") + ); + setSyncFromPrefListener("warnAddonInstall", () => + this.readWarnAddonInstall() + ); + setSyncFromPrefListener("enableOCSP", () => this.readEnableOCSP()); + setSyncToPrefListener("enableOCSP", () => this.writeEnableOCSP()); + + if (AlertsServiceDND) { + let notificationsDoNotDisturbBox = document.getElementById( + "notificationsDoNotDisturbBox" + ); + notificationsDoNotDisturbBox.removeAttribute("hidden"); + let checkbox = document.getElementById("notificationsDoNotDisturb"); + document.l10n.setAttributes(checkbox, "permissions-notification-pause"); + if (AlertsServiceDND.manualDoNotDisturb) { + let notificationsDoNotDisturb = document.getElementById( + "notificationsDoNotDisturb" + ); + notificationsDoNotDisturb.setAttribute("checked", true); + } + } + + this._initAddressBar(); + + this.initSiteDataControls(); + setEventListener( + "clearSiteDataButton", + "command", + gPrivacyPane.clearSiteData + ); + setEventListener( + "siteDataSettings", + "command", + gPrivacyPane.showSiteDataSettings + ); + + this.initCookieBannerHandling(); + + this.initDataCollection(); + + if (AppConstants.MOZ_DATA_REPORTING) { + if (AppConstants.MOZ_CRASHREPORTER) { + this.initSubmitCrashes(); + } + this.initSubmitHealthReport(); + setEventListener( + "submitHealthReportBox", + "command", + gPrivacyPane.updateSubmitHealthReport + ); + setEventListener( + "telemetryDataDeletionLearnMore", + "click", + gPrivacyPane.showDataDeletion + ); + if (AppConstants.MOZ_NORMANDY) { + this.initOptOutStudyCheckbox(); + } + this.initAddonRecommendationsCheckbox(); + } + + let signonBundle = document.getElementById("signonBundle"); + let pkiBundle = document.getElementById("pkiBundle"); + appendSearchKeywords("showPasswords", [ + signonBundle.getString("loginsDescriptionAll2"), + ]); + appendSearchKeywords("viewSecurityDevicesButton", [ + pkiBundle.getString("enable_fips"), + ]); + + if (!PrivateBrowsingUtils.enabled) { + document.getElementById("privateBrowsingAutoStart").hidden = true; + document.querySelector("menuitem[value='dontremember']").hidden = true; + } + + /* init HTTPS-Only mode */ + this.initHttpsOnly(); + + this.initDoH(); + + // Notify observers that the UI is now ready + Services.obs.notifyObservers(window, "privacy-pane-loaded"); + }, + + initSiteDataControls() { + Services.obs.addObserver(this, "sitedatamanager:sites-updated"); + Services.obs.addObserver(this, "sitedatamanager:updating-sites"); + let unload = () => { + window.removeEventListener("unload", unload); + Services.obs.removeObserver(this, "sitedatamanager:sites-updated"); + Services.obs.removeObserver(this, "sitedatamanager:updating-sites"); + }; + window.addEventListener("unload", unload); + SiteDataManager.updateSites(); + }, + + // CONTENT BLOCKING + + /** + * Initializes the content blocking section. + */ + initContentBlocking() { + setEventListener( + "contentBlockingTrackingProtectionCheckbox", + "command", + this.trackingProtectionWritePrefs + ); + setEventListener( + "contentBlockingTrackingProtectionCheckbox", + "command", + this._updateTrackingProtectionUI + ); + setEventListener( + "contentBlockingCryptominersCheckbox", + "command", + this.updateCryptominingLists + ); + setEventListener( + "contentBlockingFingerprintersCheckbox", + "command", + this.updateFingerprintingLists + ); + setEventListener( + "trackingProtectionMenu", + "command", + this.trackingProtectionWritePrefs + ); + setEventListener("standardArrow", "command", this.toggleExpansion); + setEventListener("strictArrow", "command", this.toggleExpansion); + setEventListener("customArrow", "command", this.toggleExpansion); + + Preferences.get("network.cookie.cookieBehavior").on( + "change", + gPrivacyPane.readBlockCookies.bind(gPrivacyPane) + ); + Preferences.get("browser.contentblocking.category").on( + "change", + gPrivacyPane.highlightCBCategory + ); + + // If any relevant content blocking pref changes, show a warning that the changes will + // not be implemented until they refresh their tabs. + for (let pref of CONTENT_BLOCKING_PREFS) { + Preferences.get(pref).on("change", gPrivacyPane.maybeNotifyUserToReload); + // If the value changes, run populateCategoryContents, since that change might have been + // triggered by a default value changing in the standard category. + Preferences.get(pref).on("change", gPrivacyPane.populateCategoryContents); + } + Preferences.get("urlclassifier.trackingTable").on( + "change", + gPrivacyPane.maybeNotifyUserToReload + ); + for (let button of document.querySelectorAll(".reload-tabs-button")) { + button.addEventListener("command", gPrivacyPane.reloadAllOtherTabs); + } + + let cryptoMinersOption = document.getElementById( + "contentBlockingCryptominersOption" + ); + let fingerprintersOption = document.getElementById( + "contentBlockingFingerprintersOption" + ); + let trackingAndIsolateOption = document.querySelector( + "#blockCookiesMenu menuitem[value='trackers-plus-isolate']" + ); + cryptoMinersOption.hidden = !Services.prefs.getBoolPref( + "browser.contentblocking.cryptomining.preferences.ui.enabled" + ); + fingerprintersOption.hidden = !Services.prefs.getBoolPref( + "browser.contentblocking.fingerprinting.preferences.ui.enabled" + ); + let updateTrackingAndIsolateOption = () => { + trackingAndIsolateOption.hidden = + !Services.prefs.getBoolPref( + "browser.contentblocking.reject-and-isolate-cookies.preferences.ui.enabled", + false + ) || gIsFirstPartyIsolated; + }; + Preferences.get("privacy.firstparty.isolate").on( + "change", + updateTrackingAndIsolateOption + ); + updateTrackingAndIsolateOption(); + + Preferences.get("browser.contentblocking.features.strict").on( + "change", + this.populateCategoryContents + ); + this.populateCategoryContents(); + this.highlightCBCategory(); + this.readBlockCookies(); + + // Toggles the text "Cross-site and social media trackers" based on the + // social tracking pref. If the pref is false, the text reads + // "Cross-site trackers". + const STP_COOKIES_PREF = "privacy.socialtracking.block_cookies.enabled"; + if (Services.prefs.getBoolPref(STP_COOKIES_PREF)) { + let contentBlockOptionSocialMedia = document.getElementById( + "blockCookiesSocialMedia" + ); + + document.l10n.setAttributes( + contentBlockOptionSocialMedia, + "sitedata-option-block-cross-site-tracking-cookies" + ); + } + + setUpContentBlockingWarnings(); + + initTCPStandardSection(); + }, + + populateCategoryContents() { + for (let type of ["strict", "standard"]) { + let rulesArray = []; + let selector; + if (type == "strict") { + selector = "#contentBlockingOptionStrict"; + rulesArray = Services.prefs + .getStringPref("browser.contentblocking.features.strict") + .split(","); + if (gIsFirstPartyIsolated) { + let idx = rulesArray.indexOf("cookieBehavior5"); + if (idx != -1) { + rulesArray[idx] = "cookieBehavior4"; + } + } + } else { + selector = "#contentBlockingOptionStandard"; + // In standard show/hide UI items based on the default values of the relevant prefs. + let defaults = Services.prefs.getDefaultBranch(""); + + let cookieBehavior = defaults.getIntPref( + "network.cookie.cookieBehavior" + ); + switch (cookieBehavior) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + rulesArray.push("cookieBehavior0"); + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN: + rulesArray.push("cookieBehavior1"); + break; + case Ci.nsICookieService.BEHAVIOR_REJECT: + rulesArray.push("cookieBehavior2"); + break; + case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN: + rulesArray.push("cookieBehavior3"); + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + rulesArray.push("cookieBehavior4"); + break; + case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + rulesArray.push( + gIsFirstPartyIsolated ? "cookieBehavior4" : "cookieBehavior5" + ); + break; + } + let cookieBehaviorPBM = defaults.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ); + switch (cookieBehaviorPBM) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + rulesArray.push("cookieBehaviorPBM0"); + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN: + rulesArray.push("cookieBehaviorPBM1"); + break; + case Ci.nsICookieService.BEHAVIOR_REJECT: + rulesArray.push("cookieBehaviorPBM2"); + break; + case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN: + rulesArray.push("cookieBehaviorPBM3"); + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + rulesArray.push("cookieBehaviorPBM4"); + break; + case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + rulesArray.push( + gIsFirstPartyIsolated + ? "cookieBehaviorPBM4" + : "cookieBehaviorPBM5" + ); + break; + } + rulesArray.push( + defaults.getBoolPref( + "privacy.trackingprotection.cryptomining.enabled" + ) + ? "cm" + : "-cm" + ); + rulesArray.push( + defaults.getBoolPref( + "privacy.trackingprotection.fingerprinting.enabled" + ) + ? "fp" + : "-fp" + ); + rulesArray.push( + Services.prefs.getBoolPref( + "privacy.socialtracking.block_cookies.enabled" + ) + ? "stp" + : "-stp" + ); + rulesArray.push( + defaults.getBoolPref("privacy.trackingprotection.enabled") + ? "tp" + : "-tp" + ); + rulesArray.push( + defaults.getBoolPref("privacy.trackingprotection.pbmode.enabled") + ? "tpPrivate" + : "-tpPrivate" + ); + } + + // Hide all cookie options first, until we learn which one should be showing. + document.querySelector(selector + " .all-cookies-option").hidden = true; + document.querySelector( + selector + " .unvisited-cookies-option" + ).hidden = true; + document.querySelector( + selector + " .cross-site-cookies-option" + ).hidden = true; + document.querySelector( + selector + " .third-party-tracking-cookies-option" + ).hidden = true; + document.querySelector( + selector + " .all-third-party-cookies-private-windows-option" + ).hidden = true; + document.querySelector( + selector + " .all-third-party-cookies-option" + ).hidden = true; + document.querySelector(selector + " .social-media-option").hidden = true; + + for (let item of rulesArray) { + // Note "cookieBehavior0", will result in no UI changes, so is not listed here. + switch (item) { + case "tp": + document.querySelector( + selector + " .trackers-option" + ).hidden = false; + break; + case "-tp": + document.querySelector( + selector + " .trackers-option" + ).hidden = true; + break; + case "tpPrivate": + document.querySelector( + selector + " .pb-trackers-option" + ).hidden = false; + break; + case "-tpPrivate": + document.querySelector( + selector + " .pb-trackers-option" + ).hidden = true; + break; + case "fp": + document.querySelector( + selector + " .fingerprinters-option" + ).hidden = false; + break; + case "-fp": + document.querySelector( + selector + " .fingerprinters-option" + ).hidden = true; + break; + case "cm": + document.querySelector( + selector + " .cryptominers-option" + ).hidden = false; + break; + case "-cm": + document.querySelector( + selector + " .cryptominers-option" + ).hidden = true; + break; + case "stp": + // Store social tracking cookies pref + const STP_COOKIES_PREF = + "privacy.socialtracking.block_cookies.enabled"; + + if (Services.prefs.getBoolPref(STP_COOKIES_PREF)) { + document.querySelector( + selector + " .social-media-option" + ).hidden = false; + } + break; + case "-stp": + // Store social tracking cookies pref + document.querySelector( + selector + " .social-media-option" + ).hidden = true; + break; + case "cookieBehavior1": + document.querySelector( + selector + " .all-third-party-cookies-option" + ).hidden = false; + break; + case "cookieBehavior2": + document.querySelector( + selector + " .all-cookies-option" + ).hidden = false; + break; + case "cookieBehavior3": + document.querySelector( + selector + " .unvisited-cookies-option" + ).hidden = false; + break; + case "cookieBehavior4": + document.querySelector( + selector + " .third-party-tracking-cookies-option" + ).hidden = false; + break; + case "cookieBehavior5": + document.querySelector( + selector + " .cross-site-cookies-option" + ).hidden = false; + break; + case "cookieBehaviorPBM5": + // We only need to show the cookie option for private windows if the + // cookieBehaviors are different between regular windows and private + // windows. + if (!rulesArray.includes("cookieBehavior5")) { + document.querySelector( + selector + " .all-third-party-cookies-private-windows-option" + ).hidden = false; + } + break; + } + } + // Hide the "tracking protection in private browsing" list item + // if the "tracking protection enabled in all windows" list item is showing. + if (!document.querySelector(selector + " .trackers-option").hidden) { + document.querySelector(selector + " .pb-trackers-option").hidden = true; + } + } + }, + + highlightCBCategory() { + let value = Preferences.get("browser.contentblocking.category").value; + let standardEl = document.getElementById("contentBlockingOptionStandard"); + let strictEl = document.getElementById("contentBlockingOptionStrict"); + let customEl = document.getElementById("contentBlockingOptionCustom"); + standardEl.classList.remove("selected"); + strictEl.classList.remove("selected"); + customEl.classList.remove("selected"); + + switch (value) { + case "strict": + strictEl.classList.add("selected"); + break; + case "custom": + customEl.classList.add("selected"); + break; + case "standard": + /* fall through */ + default: + standardEl.classList.add("selected"); + break; + } + }, + + updateCryptominingLists() { + let listPrefs = [ + "urlclassifier.features.cryptomining.blacklistTables", + "urlclassifier.features.cryptomining.whitelistTables", + ]; + + let listValue = listPrefs + .map(l => Services.prefs.getStringPref(l)) + .join(","); + listManager.forceUpdates(listValue); + }, + + updateFingerprintingLists() { + let listPrefs = [ + "urlclassifier.features.fingerprinting.blacklistTables", + "urlclassifier.features.fingerprinting.whitelistTables", + ]; + + let listValue = listPrefs + .map(l => Services.prefs.getStringPref(l)) + .join(","); + listManager.forceUpdates(listValue); + }, + + // TRACKING PROTECTION MODE + + /** + * Selects the right item of the Tracking Protection menulist and checkbox. + */ + trackingProtectionReadPrefs() { + let enabledPref = Preferences.get("privacy.trackingprotection.enabled"); + let pbmPref = Preferences.get("privacy.trackingprotection.pbmode.enabled"); + let tpMenu = document.getElementById("trackingProtectionMenu"); + let tpCheckbox = document.getElementById( + "contentBlockingTrackingProtectionCheckbox" + ); + + this._updateTrackingProtectionUI(); + + // Global enable takes precedence over enabled in Private Browsing. + if (enabledPref.value) { + tpMenu.value = "always"; + tpCheckbox.checked = true; + } else if (pbmPref.value) { + tpMenu.value = "private"; + tpCheckbox.checked = true; + } else { + tpMenu.value = "never"; + tpCheckbox.checked = false; + } + }, + + /** + * Selects the right items of the new Cookies & Site Data UI. + */ + networkCookieBehaviorReadPrefs() { + let behavior = Services.cookies.getCookieBehavior(false); + let blockCookiesMenu = document.getElementById("blockCookiesMenu"); + let deleteOnCloseCheckbox = document.getElementById("deleteOnClose"); + let deleteOnCloseNote = document.getElementById("deleteOnCloseNote"); + let blockCookies = behavior != Ci.nsICookieService.BEHAVIOR_ACCEPT; + let cookieBehaviorLocked = Services.prefs.prefIsLocked( + "network.cookie.cookieBehavior" + ); + let blockCookiesControlsDisabled = !blockCookies || cookieBehaviorLocked; + blockCookiesMenu.disabled = blockCookiesControlsDisabled; + + let completelyBlockCookies = + behavior == Ci.nsICookieService.BEHAVIOR_REJECT; + let privateBrowsing = Preferences.get( + "browser.privatebrowsing.autostart" + ).value; + deleteOnCloseCheckbox.disabled = privateBrowsing || completelyBlockCookies; + deleteOnCloseNote.hidden = !privateBrowsing; + + switch (behavior) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN: + blockCookiesMenu.value = "all-third-parties"; + break; + case Ci.nsICookieService.BEHAVIOR_REJECT: + blockCookiesMenu.value = "always"; + break; + case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN: + blockCookiesMenu.value = "unvisited"; + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + blockCookiesMenu.value = "trackers"; + break; + case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + blockCookiesMenu.value = "trackers-plus-isolate"; + break; + } + }, + + /** + * Sets the pref values based on the selected item of the radiogroup. + */ + trackingProtectionWritePrefs() { + let enabledPref = Preferences.get("privacy.trackingprotection.enabled"); + let pbmPref = Preferences.get("privacy.trackingprotection.pbmode.enabled"); + let stpPref = Preferences.get( + "privacy.trackingprotection.socialtracking.enabled" + ); + let stpCookiePref = Preferences.get( + "privacy.socialtracking.block_cookies.enabled" + ); + // Currently, we don't expose the email tracking protection setting on our + // privacy UI. Instead, we use the existing tracking protection checkbox to + // control the email tracking protection. + let emailTPPref = Preferences.get( + "privacy.trackingprotection.emailtracking.enabled" + ); + let emailTPPBMPref = Preferences.get( + "privacy.trackingprotection.emailtracking.pbmode.enabled" + ); + let tpMenu = document.getElementById("trackingProtectionMenu"); + let tpCheckbox = document.getElementById( + "contentBlockingTrackingProtectionCheckbox" + ); + + let value; + if (tpCheckbox.checked) { + if (tpMenu.value == "never") { + tpMenu.value = "private"; + } + value = tpMenu.value; + } else { + tpMenu.value = "never"; + value = "never"; + } + + switch (value) { + case "always": + enabledPref.value = true; + pbmPref.value = true; + emailTPPref.value = true; + emailTPPBMPref.value = true; + if (stpCookiePref.value) { + stpPref.value = true; + } + break; + case "private": + enabledPref.value = false; + pbmPref.value = true; + emailTPPref.value = false; + emailTPPBMPref.value = true; + if (stpCookiePref.value) { + stpPref.value = false; + } + break; + case "never": + enabledPref.value = false; + pbmPref.value = false; + emailTPPref.value = false; + emailTPPBMPref.value = false; + if (stpCookiePref.value) { + stpPref.value = false; + } + break; + } + }, + + toggleExpansion(e) { + let carat = e.target; + carat.classList.toggle("up"); + carat.closest(".privacy-detailedoption").classList.toggle("expanded"); + carat.setAttribute( + "aria-expanded", + carat.getAttribute("aria-expanded") === "false" + ); + }, + + // HISTORY MODE + + /** + * The list of preferences which affect the initial history mode settings. + * If the auto start private browsing mode pref is active, the initial + * history mode would be set to "Don't remember anything". + * If ALL of these preferences are set to the values that correspond + * to keeping some part of history, and the auto-start + * private browsing mode is not active, the initial history mode would be + * set to "Remember everything". + * Otherwise, the initial history mode would be set to "Custom". + * + * Extensions adding their own preferences can set values here if needed. + */ + prefsForKeepingHistory: { + "places.history.enabled": true, // History is enabled + "browser.formfill.enable": true, // Form information is saved + "privacy.sanitize.sanitizeOnShutdown": false, // Private date is NOT cleared on shutdown + }, + + /** + * The list of control IDs which are dependent on the auto-start private + * browsing setting, such that in "Custom" mode they would be disabled if + * the auto-start private browsing checkbox is checked, and enabled otherwise. + * + * Extensions adding their own controls can append their IDs to this array if needed. + */ + dependentControls: [ + "rememberHistory", + "rememberForms", + "alwaysClear", + "clearDataSettings", + ], + + /** + * Check whether preferences values are set to keep history + * + * @param aPrefs an array of pref names to check for + * @returns boolean true if all of the prefs are set to keep history, + * false otherwise + */ + _checkHistoryValues(aPrefs) { + for (let pref of Object.keys(aPrefs)) { + if (Preferences.get(pref).value != aPrefs[pref]) { + return false; + } + } + return true; + }, + + /** + * Initialize the history mode menulist based on the privacy preferences + */ + initializeHistoryMode() { + let mode; + let getVal = aPref => Preferences.get(aPref).value; + + if (getVal("privacy.history.custom")) { + mode = "custom"; + } else if (this._checkHistoryValues(this.prefsForKeepingHistory)) { + if (getVal("browser.privatebrowsing.autostart")) { + mode = "dontremember"; + } else { + mode = "remember"; + } + } else { + mode = "custom"; + } + + document.getElementById("historyMode").value = mode; + }, + + /** + * Update the selected pane based on the history mode menulist + */ + updateHistoryModePane() { + let selectedIndex = -1; + switch (document.getElementById("historyMode").value) { + case "remember": + selectedIndex = 0; + break; + case "dontremember": + selectedIndex = 1; + break; + case "custom": + selectedIndex = 2; + break; + } + document.getElementById("historyPane").selectedIndex = selectedIndex; + Preferences.get("privacy.history.custom").value = selectedIndex == 2; + }, + + /** + * Update the private browsing auto-start pref and the history mode + * micro-management prefs based on the history mode menulist + */ + updateHistoryModePrefs() { + let pref = Preferences.get("browser.privatebrowsing.autostart"); + switch (document.getElementById("historyMode").value) { + case "remember": + if (pref.value) { + pref.value = false; + } + + // select the remember history option if needed + Preferences.get("places.history.enabled").value = true; + + // select the remember forms history option + Preferences.get("browser.formfill.enable").value = true; + + // select the clear on close option + Preferences.get("privacy.sanitize.sanitizeOnShutdown").value = false; + break; + case "dontremember": + if (!pref.value) { + pref.value = true; + } + break; + } + }, + + /** + * Update the privacy micro-management controls based on the + * value of the private browsing auto-start preference. + */ + updatePrivacyMicroControls() { + let clearDataSettings = document.getElementById("clearDataSettings"); + + if (document.getElementById("historyMode").value == "custom") { + let disabled = Preferences.get("browser.privatebrowsing.autostart").value; + this.dependentControls.forEach(aElement => { + let control = document.getElementById(aElement); + let preferenceId = control.getAttribute("preference"); + if (!preferenceId) { + let dependentControlId = control.getAttribute("control"); + if (dependentControlId) { + let dependentControl = document.getElementById(dependentControlId); + preferenceId = dependentControl.getAttribute("preference"); + } + } + + let preference = preferenceId ? Preferences.get(preferenceId) : {}; + control.disabled = disabled || preference.locked; + if (control != clearDataSettings) { + this.ensurePrivacyMicroControlUncheckedWhenDisabled(control); + } + }); + + clearDataSettings.removeAttribute("hidden"); + + if (!disabled) { + // adjust the Settings button for sanitizeOnShutdown + this._updateSanitizeSettingsButton(); + } + } else { + clearDataSettings.hidden = true; + } + }, + + ensurePrivacyMicroControlUncheckedWhenDisabled(el) { + if (Preferences.get("browser.privatebrowsing.autostart").value) { + // Set checked to false when called from updatePrivacyMicroControls + el.checked = false; + // return false for the onsyncfrompreference case: + return false; + } + return undefined; // tell preferencesBindings to assign the 'right' value. + }, + + // CLEAR PRIVATE DATA + + /* + * Preferences: + * + * privacy.sanitize.sanitizeOnShutdown + * - true if the user's private data is cleared on startup according to the + * Clear Private Data settings, false otherwise + */ + + /** + * Displays the Clear Private Data settings dialog. + */ + showClearPrivateDataSettings() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sanitize.xhtml", + { features: "resizable=no" } + ); + }, + + /** + * Displays a dialog from which individual parts of private data may be + * cleared. + */ + clearPrivateDataNow(aClearEverything) { + var ts = Preferences.get("privacy.sanitize.timeSpan"); + var timeSpanOrig = ts.value; + + if (aClearEverything) { + ts.value = 0; + } + + gSubDialog.open("chrome://browser/content/sanitize.xhtml", { + features: "resizable=no", + closingCallback: () => { + // reset the timeSpan pref + if (aClearEverything) { + ts.value = timeSpanOrig; + } + + Services.obs.notifyObservers(null, "clear-private-data"); + }, + }); + }, + + /* + * On loading the page, assigns the state to the deleteOnClose checkbox that fits the pref selection + */ + initDeleteOnCloseBox() { + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + deleteOnCloseBox.checked = + (Preferences.get("privacy.sanitize.sanitizeOnShutdown").value && + Preferences.get("privacy.clearOnShutdown.cookies").value && + Preferences.get("privacy.clearOnShutdown.cache").value && + Preferences.get("privacy.clearOnShutdown.offlineApps").value) || + Preferences.get("browser.privatebrowsing.autostart").value; + }, + + /* + * Keeps the state of the deleteOnClose checkbox in sync with the pref selection + */ + syncSanitizationPrefsWithDeleteOnClose() { + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let historyMode = Preferences.get("privacy.history.custom"); + let sanitizeOnShutdownPref = Preferences.get( + "privacy.sanitize.sanitizeOnShutdown" + ); + // ClearOnClose cleaning categories + let cookiePref = Preferences.get("privacy.clearOnShutdown.cookies"); + let cachePref = Preferences.get("privacy.clearOnShutdown.cache"); + let offlineAppsPref = Preferences.get( + "privacy.clearOnShutdown.offlineApps" + ); + + // Sync the cleaning prefs with the deleteOnClose box + deleteOnCloseBox.addEventListener("command", () => { + let { checked } = deleteOnCloseBox; + cookiePref.value = checked; + cachePref.value = checked; + offlineAppsPref.value = checked; + // Forget the current pref selection if sanitizeOnShutdown is disabled, + // to not over clear when it gets enabled by the sync mechanism + if (!sanitizeOnShutdownPref.value) { + this._resetCleaningPrefs(); + } + // If no other cleaning category is selected, sanitizeOnShutdown gets synced with deleteOnClose + sanitizeOnShutdownPref.value = + this._isCustomCleaningPrefPresent() || checked; + + // Update the view of the history settings + if (checked && !historyMode.value) { + historyMode.value = "custom"; + this.initializeHistoryMode(); + this.updateHistoryModePane(); + this.updatePrivacyMicroControls(); + } + }); + + cookiePref.on("change", this._onSanitizePrefChangeSyncClearOnClose); + cachePref.on("change", this._onSanitizePrefChangeSyncClearOnClose); + offlineAppsPref.on("change", this._onSanitizePrefChangeSyncClearOnClose); + sanitizeOnShutdownPref.on( + "change", + this._onSanitizePrefChangeSyncClearOnClose + ); + }, + + /* + * Sync the deleteOnClose box to its cleaning prefs + */ + _onSanitizePrefChangeSyncClearOnClose() { + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + deleteOnCloseBox.checked = + Preferences.get("privacy.clearOnShutdown.cookies").value && + Preferences.get("privacy.clearOnShutdown.cache").value && + Preferences.get("privacy.clearOnShutdown.offlineApps").value && + Preferences.get("privacy.sanitize.sanitizeOnShutdown").value; + }, + + /* + * Unsets cleaning prefs that do not belong to DeleteOnClose + */ + _resetCleaningPrefs() { + SANITIZE_ON_SHUTDOWN_PREFS_ONLY.forEach( + pref => (Preferences.get(pref).value = false) + ); + }, + + /* + Checks if the user set cleaning prefs that do not belong to DeleteOnClose + */ + _isCustomCleaningPrefPresent() { + return SANITIZE_ON_SHUTDOWN_PREFS_ONLY.some( + pref => Preferences.get(pref).value + ); + }, + + /** + * Enables or disables the "Settings..." button depending + * on the privacy.sanitize.sanitizeOnShutdown preference value + */ + _updateSanitizeSettingsButton() { + var settingsButton = document.getElementById("clearDataSettings"); + var sanitizeOnShutdownPref = Preferences.get( + "privacy.sanitize.sanitizeOnShutdown" + ); + + settingsButton.disabled = !sanitizeOnShutdownPref.value; + }, + + toggleDoNotDisturbNotifications(event) { + AlertsServiceDND.manualDoNotDisturb = event.target.checked; + }, + + // PRIVATE BROWSING + + /** + * Initialize the starting state for the auto-start private browsing mode pref reverter. + */ + initAutoStartPrivateBrowsingReverter() { + // We determine the mode in initializeHistoryMode, which is guaranteed to have been + // called before now, so this is up-to-date. + let mode = document.getElementById("historyMode"); + this._lastMode = mode.selectedIndex; + // The value of the autostart pref, on the other hand, is gotten from Preferences, + // which updates the DOM asynchronously, so we can't rely on the DOM. Get it directly + // from the prefs. + this._lastCheckState = Preferences.get( + "browser.privatebrowsing.autostart" + ).value; + }, + + _lastMode: null, + _lastCheckState: null, + async updateAutostart() { + let mode = document.getElementById("historyMode"); + let autoStart = document.getElementById("privateBrowsingAutoStart"); + let pref = Preferences.get("browser.privatebrowsing.autostart"); + if ( + (mode.value == "custom" && this._lastCheckState == autoStart.checked) || + (mode.value == "remember" && !this._lastCheckState) || + (mode.value == "dontremember" && this._lastCheckState) + ) { + // These are all no-op changes, so we don't need to prompt. + this._lastMode = mode.selectedIndex; + this._lastCheckState = autoStart.hasAttribute("checked"); + return; + } + + if (!this._shouldPromptForRestart) { + // We're performing a revert. Just let it happen. + return; + } + + let buttonIndex = await confirmRestartPrompt( + autoStart.checked, + 1, + true, + false + ); + if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) { + pref.value = autoStart.hasAttribute("checked"); + Services.startup.quit( + Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart + ); + return; + } + + this._shouldPromptForRestart = false; + + if (this._lastCheckState) { + autoStart.checked = "checked"; + } else { + autoStart.removeAttribute("checked"); + } + pref.value = autoStart.hasAttribute("checked"); + mode.selectedIndex = this._lastMode; + mode.doCommand(); + + this._shouldPromptForRestart = true; + }, + + /** + * Displays fine-grained, per-site preferences for tracking protection. + */ + showTrackingProtectionExceptions() { + let params = { + permissionType: "trackingprotection", + disableETPVisible: true, + prefilledHost: "", + hideStatusColumn: true, + }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + undefined, + params + ); + }, + + /** + * Displays the available block lists for tracking protection. + */ + showBlockLists() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/blocklists.xhtml" + ); + }, + + // COOKIES AND SITE DATA + + /* + * Preferences: + * + * network.cookie.cookieBehavior + * - determines how the browser should handle cookies: + * 0 means enable all cookies + * 1 means reject all third party cookies + * 2 means disable all cookies + * 3 means reject third party cookies unless at least one is already set for the eTLD + * 4 means reject all trackers + * 5 means reject all trackers and partition third-party cookies + * see netwerk/cookie/src/CookieService.cpp for details + */ + + /** + * Reads the network.cookie.cookieBehavior preference value and + * enables/disables the "blockCookiesMenu" menulist accordingly. + */ + readBlockCookies() { + let bcControl = document.getElementById("blockCookiesMenu"); + bcControl.disabled = + Services.cookies.getCookieBehavior(false) == + Ci.nsICookieService.BEHAVIOR_ACCEPT; + }, + + /** + * Updates the "accept third party cookies" menu based on whether the + * "contentBlockingBlockCookiesCheckbox" checkbox is checked. + */ + writeBlockCookies() { + let block = document.getElementById("contentBlockingBlockCookiesCheckbox"); + let blockCookiesMenu = document.getElementById("blockCookiesMenu"); + + if (block.checked) { + // Automatically select 'third-party trackers' as the default. + blockCookiesMenu.selectedIndex = 0; + return this.writeBlockCookiesFrom(); + } + return Ci.nsICookieService.BEHAVIOR_ACCEPT; + }, + + readBlockCookiesFrom() { + switch (Services.cookies.getCookieBehavior(false)) { + case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN: + return "all-third-parties"; + case Ci.nsICookieService.BEHAVIOR_REJECT: + return "always"; + case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN: + return "unvisited"; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + return "trackers"; + case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + return "trackers-plus-isolate"; + default: + return undefined; + } + }, + + writeBlockCookiesFrom() { + let block = document.getElementById("blockCookiesMenu").selectedItem; + switch (block.value) { + case "trackers": + return Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + case "unvisited": + return Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN; + case "always": + return Ci.nsICookieService.BEHAVIOR_REJECT; + case "all-third-parties": + return Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN; + case "trackers-plus-isolate": + return Ci.nsICookieService + .BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + default: + return undefined; + } + }, + + /** + * Discard the browsers of all tabs in all windows. Pinned tabs, as + * well as tabs for which discarding doesn't succeed (e.g. selected + * tabs, tabs with beforeunload listeners), are reloaded. + */ + reloadAllOtherTabs() { + let ourTab = BrowserWindowTracker.getTopWindow().gBrowser.selectedTab; + BrowserWindowTracker.orderedWindows.forEach(win => { + let otherGBrowser = win.gBrowser; + for (let tab of otherGBrowser.tabs) { + if (tab == ourTab) { + // Don't reload our preferences tab. + continue; + } + + if (tab.pinned || tab.selected) { + otherGBrowser.reloadTab(tab); + } else { + otherGBrowser.discardBrowser(tab); + } + } + }); + + for (let notification of document.querySelectorAll(".reload-tabs")) { + notification.hidden = true; + } + }, + + /** + * If there are more tabs than just the preferences tab, show a warning to the user that + * they need to reload their tabs to apply the setting. + */ + maybeNotifyUserToReload() { + let shouldShow = false; + if (window.BrowserWindowTracker.orderedWindows.length > 1) { + shouldShow = true; + } else { + let tabbrowser = window.BrowserWindowTracker.getTopWindow().gBrowser; + if (tabbrowser.tabs.length > 1) { + shouldShow = true; + } + } + if (shouldShow) { + for (let notification of document.querySelectorAll(".reload-tabs")) { + notification.hidden = false; + } + } + }, + + /** + * Displays fine-grained, per-site preferences for cookies. + */ + showCookieExceptions() { + var params = { + blockVisible: true, + sessionVisible: true, + allowVisible: true, + prefilledHost: "", + permissionType: "cookie", + }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + undefined, + params + ); + }, + + /** + * Displays per-site preferences for HTTPS-Only Mode exceptions. + */ + showHttpsOnlyModeExceptions() { + var params = { + blockVisible: false, + sessionVisible: true, + allowVisible: false, + prefilledHost: "", + permissionType: "https-only-load-insecure", + }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + undefined, + params + ); + }, + + showDoHExceptions() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/dohExceptions.xhtml", + undefined + ); + }, + + showSiteDataSettings() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml" + ); + }, + + toggleSiteData(shouldShow) { + let clearButton = document.getElementById("clearSiteDataButton"); + let settingsButton = document.getElementById("siteDataSettings"); + clearButton.disabled = !shouldShow; + settingsButton.disabled = !shouldShow; + }, + + showSiteDataLoading() { + let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize"); + document.l10n.setAttributes( + totalSiteDataSizeLabel, + "sitedata-total-size-calculating" + ); + }, + + updateTotalDataSizeLabel(siteDataUsage) { + SiteDataManager.getCacheSize().then(function (cacheUsage) { + let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize"); + let totalUsage = siteDataUsage + cacheUsage; + let [value, unit] = DownloadUtils.convertByteUnits(totalUsage); + document.l10n.setAttributes( + totalSiteDataSizeLabel, + "sitedata-total-size", + { + value, + unit, + } + ); + }); + }, + + clearSiteData() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml" + ); + }, + + /** + * Initializes the cookie banner handling subgroup on the privacy pane. + * + * This UI is shown if the "cookiebanners.ui.desktop.enabled" pref is true. + * + * The cookie banner handling checkbox reflects the cookie banner feature + * state. It is enabled when the service enabled via the + * cookiebanners.service.mode pref. If detection-only mode is enabled the + * checkbox is unchecked, since in this mode no banners are handled. It is + * only used for detection for banners which means we may prompt the user to + * enable the feature via other UI surfaces such as the onboarding doorhanger. + * + * If the user checks the checkbox, the pref value is set to + * nsICookieBannerService.MODE_REJECT_OR_ACCEPT. + * + * If the user unchecks the checkbox, the mode pref value is set to + * nsICookieBannerService.MODE_DISABLED. + * + * Advanced users can choose other int-valued modes via about:config. + */ + initCookieBannerHandling() { + setSyncFromPrefListener("handleCookieBanners", () => + this.readCookieBannerMode() + ); + setSyncToPrefListener("handleCookieBanners", () => + this.writeCookieBannerMode() + ); + + let preference = Preferences.get("cookiebanners.ui.desktop.enabled"); + preference.on("change", () => this.updateCookieBannerHandlingVisibility()); + + this.updateCookieBannerHandlingVisibility(); + }, + + /** + * Reads the cookiebanners.service.mode and detectOnly preference value and + * updates the cookie banner handling checkbox accordingly. + */ + readCookieBannerMode() { + if (Preferences.get("cookiebanners.service.detectOnly").value) { + return false; + } + return ( + Preferences.get("cookiebanners.service.mode").value != + Ci.nsICookieBannerService.MODE_DISABLED + ); + }, + + /** + * Translates user clicks on the cookie banner handling checkbox to the + * corresponding integer-valued cookie banner mode preference. + */ + writeCookieBannerMode() { + let checkbox = document.getElementById("handleCookieBanners"); + let mode; + if (checkbox.checked) { + mode = Ci.nsICookieBannerService.MODE_REJECT; + + // Also unset the detect-only mode pref, just in case the user enabled + // the feature via about:preferences, not the onboarding doorhanger. + Services.prefs.setBoolPref("cookiebanners.service.detectOnly", false); + } else { + mode = Ci.nsICookieBannerService.MODE_DISABLED; + } + + /** + * There is a second service.mode pref for private browsing, + * but for now we want it always be the same as service.mode + * more info: https://bugzilla.mozilla.org/show_bug.cgi?id=1817201 + */ + Services.prefs.setIntPref( + "cookiebanners.service.mode.privateBrowsing", + mode + ); + return mode; + }, + + /** + * Shows or hides the cookie banner handling section based on the value of + * the "cookiebanners.ui.desktop.enabled" pref. + */ + updateCookieBannerHandlingVisibility() { + let groupbox = document.getElementById("cookieBannerHandlingGroup"); + let isEnabled = Preferences.get("cookiebanners.ui.desktop.enabled").value; + + // Because the top-level pane showing code unsets the hidden attribute, we + // manually hide the section when cookie banner handling is preffed off. + if (isEnabled) { + groupbox.removeAttribute("style"); + } else { + groupbox.setAttribute("style", "display: none !important"); + } + }, + + // ADDRESS BAR + + /** + * Initializes the address bar section. + */ + _initAddressBar() { + // Update the Firefox Suggest section when its Nimbus config changes. + let onNimbus = () => this._updateFirefoxSuggestSection(); + NimbusFeatures.urlbar.onUpdate(onNimbus); + window.addEventListener("unload", () => { + NimbusFeatures.urlbar.offUpdate(onNimbus); + }); + + // The Firefox Suggest info box potentially needs updating when any of the + // toggles change. + let infoBoxPrefs = [ + "browser.urlbar.suggest.quicksuggest.nonsponsored", + "browser.urlbar.suggest.quicksuggest.sponsored", + "browser.urlbar.quicksuggest.dataCollection.enabled", + ]; + for (let pref of infoBoxPrefs) { + Preferences.get(pref).on("change", () => + this._updateFirefoxSuggestInfoBox() + ); + } + + this._updateFirefoxSuggestSection(true); + this._initQuickActionsSection(); + }, + + /** + * Updates the Firefox Suggest section (in the address bar section) depending + * on whether the user is enrolled in a Firefox Suggest rollout. + * + * @param {boolean} [onInit] + * Pass true when calling this when initializing the pane. + */ + _updateFirefoxSuggestSection(onInit = false) { + // Show the best match checkbox container as appropriate. + document.getElementById("firefoxSuggestBestMatchContainer").hidden = + !UrlbarPrefs.get("bestMatchEnabled"); + + let container = document.getElementById("firefoxSuggestContainer"); + + if (UrlbarPrefs.get("quickSuggestEnabled")) { + // Update the l10n IDs of text elements. + let l10nIdByElementId = { + locationBarGroupHeader: "addressbar-header-firefox-suggest", + locationBarSuggestionLabel: "addressbar-suggest-firefox-suggest", + }; + for (let [elementId, l10nId] of Object.entries(l10nIdByElementId)) { + let element = document.getElementById(elementId); + element.dataset.l10nIdOriginal ??= element.dataset.l10nId; + element.dataset.l10nId = l10nId; + } + + // Add the extraMargin class to the engine-prefs link. + document + .getElementById("openSearchEnginePreferences") + .classList.add("extraMargin"); + + // Show the container. + this._updateFirefoxSuggestInfoBox(); + + this._updateDismissedSuggestionsStatus(); + Preferences.get(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST).on("change", () => + this._updateDismissedSuggestionsStatus() + ); + Preferences.get(PREF_URLBAR_WEATHER_USER_ENABLED).on("change", () => + this._updateDismissedSuggestionsStatus() + ); + setEventListener("restoreDismissedSuggestions", "command", () => + this.restoreDismissedSuggestions() + ); + + container.removeAttribute("hidden"); + } else if (!onInit) { + // Firefox Suggest is not enabled. This is the default, so to avoid + // accidentally messing anything up, only modify the doc if we're being + // called due to a change in the rollout-enabled status (!onInit). + container.setAttribute("hidden", "true"); + let elementIds = ["locationBarGroupHeader", "locationBarSuggestionLabel"]; + for (let id of elementIds) { + let element = document.getElementById(id); + element.dataset.l10nId = element.dataset.l10nIdOriginal; + delete element.dataset.l10nIdOriginal; + document.l10n.translateElements([element]); + } + document + .getElementById("openSearchEnginePreferences") + .classList.remove("extraMargin"); + } + }, + + /** + * Updates the Firefox Suggest info box (in the address bar section) depending + * on the states of the Firefox Suggest toggles. + */ + _updateFirefoxSuggestInfoBox() { + let nonsponsored = Preferences.get( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ).value; + let sponsored = Preferences.get( + "browser.urlbar.suggest.quicksuggest.sponsored" + ).value; + let dataCollection = Preferences.get( + "browser.urlbar.quicksuggest.dataCollection.enabled" + ).value; + + // Get the l10n ID of the appropriate text based on the values of the three + // prefs. + let l10nId; + if (nonsponsored && sponsored && dataCollection) { + l10nId = "addressbar-firefox-suggest-info-all"; + } else if (nonsponsored && sponsored && !dataCollection) { + l10nId = "addressbar-firefox-suggest-info-nonsponsored-sponsored"; + } else if (nonsponsored && !sponsored && dataCollection) { + l10nId = "addressbar-firefox-suggest-info-nonsponsored-data"; + } else if (nonsponsored && !sponsored && !dataCollection) { + l10nId = "addressbar-firefox-suggest-info-nonsponsored"; + } else if (!nonsponsored && sponsored && dataCollection) { + l10nId = "addressbar-firefox-suggest-info-sponsored-data"; + } else if (!nonsponsored && sponsored && !dataCollection) { + l10nId = "addressbar-firefox-suggest-info-sponsored"; + } else if (!nonsponsored && !sponsored && dataCollection) { + l10nId = "addressbar-firefox-suggest-info-data"; + } + + let instance = (this._firefoxSuggestInfoBoxInstance = {}); + let infoBox = document.getElementById("firefoxSuggestInfoBox"); + if (!l10nId) { + infoBox.hidden = true; + } else { + let infoText = document.getElementById("firefoxSuggestInfoText"); + infoText.dataset.l10nId = l10nId; + + // If the info box is currently hidden and we unhide it immediately, it + // will show its old text until the new text is asyncly fetched and shown. + // That's ugly, so wait for the fetch to finish before unhiding it. + document.l10n.translateElements([infoText]).then(() => { + if (instance == this._firefoxSuggestInfoBoxInstance) { + infoBox.hidden = false; + } + }); + } + }, + + /** + * Enables/disables the "Restore" button for dismissed Firefox Suggest + * suggestions. + */ + _updateDismissedSuggestionsStatus() { + document.getElementById("restoreDismissedSuggestions").disabled = + !Services.prefs.prefHasUserValue(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST) && + !( + Services.prefs.prefHasUserValue(PREF_URLBAR_WEATHER_USER_ENABLED) && + !Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED) + ); + }, + + /** + * Restores Firefox Suggest suggestions dismissed by the user. + */ + restoreDismissedSuggestions() { + Services.prefs.clearUserPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST); + Services.prefs.clearUserPref(PREF_URLBAR_WEATHER_USER_ENABLED); + }, + + // GEOLOCATION + + /** + * Displays the location exceptions dialog where specific site location + * preferences can be set. + */ + showLocationExceptions() { + let params = { permissionType: "geo" }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // XR + + /** + * Displays the XR exceptions dialog where specific site XR + * preferences can be set. + */ + showXRExceptions() { + let params = { permissionType: "xr" }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // CAMERA + + /** + * Displays the camera exceptions dialog where specific site camera + * preferences can be set. + */ + showCameraExceptions() { + let params = { permissionType: "camera" }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // MICROPHONE + + /** + * Displays the microphone exceptions dialog where specific site microphone + * preferences can be set. + */ + showMicrophoneExceptions() { + let params = { permissionType: "microphone" }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // SPEAKER + + /** + * Displays the speaker exceptions dialog where specific site speaker + * preferences can be set. + */ + showSpeakerExceptions() { + let params = { permissionType: "speaker" }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // NOTIFICATIONS + + /** + * Displays the notifications exceptions dialog where specific site notification + * preferences can be set. + */ + showNotificationExceptions() { + let params = { permissionType: "desktop-notification" }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // MEDIA + + showAutoplayMediaExceptions() { + var params = { permissionType: "autoplay-media" }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // POP-UPS + + /** + * Displays the popup exceptions dialog where specific site popup preferences + * can be set. + */ + showPopupExceptions() { + var params = { + blockVisible: false, + sessionVisible: false, + allowVisible: true, + prefilledHost: "", + permissionType: "popup", + }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + { features: "resizable=yes" }, + params + ); + }, + + // UTILITY FUNCTIONS + + /** + * Utility function to enable/disable the button specified by aButtonID based + * on the value of the Boolean preference specified by aPreferenceID. + */ + updateButtons(aButtonID, aPreferenceID) { + var button = document.getElementById(aButtonID); + var preference = Preferences.get(aPreferenceID); + button.disabled = !preference.value || preference.locked; + return undefined; + }, + + // BEGIN UI CODE + + /* + * Preferences: + * + * dom.disable_open_during_load + * - true if popups are blocked by default, false otherwise + */ + + // POP-UPS + + /** + * Displays a dialog in which the user can view and modify the list of sites + * where passwords are never saved. + */ + showPasswordExceptions() { + var params = { + blockVisible: true, + sessionVisible: false, + allowVisible: false, + hideStatusColumn: true, + prefilledHost: "", + permissionType: "login-saving", + }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + undefined, + params + ); + }, + + /** + * Initializes master password UI: the "use master password" checkbox, selects + * the master password button to show, and enables/disables it as necessary. + * The master password is controlled by various bits of NSS functionality, so + * the UI for it can't be controlled by the normal preference bindings. + */ + _initMasterPasswordUI() { + var noMP = !LoginHelper.isPrimaryPasswordSet(); + + var button = document.getElementById("changeMasterPassword"); + button.disabled = noMP; + + var checkbox = document.getElementById("useMasterPassword"); + checkbox.checked = !noMP; + checkbox.disabled = + (noMP && !Services.policies.isAllowed("createMasterPassword")) || + (!noMP && !Services.policies.isAllowed("removeMasterPassword")); + }, + + /** + * Enables/disables the master password button depending on the state of the + * "use master password" checkbox, and prompts for master password removal if + * one is set. + */ + async updateMasterPasswordButton() { + var checkbox = document.getElementById("useMasterPassword"); + var button = document.getElementById("changeMasterPassword"); + button.disabled = !checkbox.checked; + + // unchecking the checkbox should try to immediately remove the master + // password, because it's impossible to non-destructively remove the master + // password used to encrypt all the passwords without providing it (by + // design), and it would be extremely odd to pop up that dialog when the + // user closes the prefwindow and saves his settings + if (!checkbox.checked) { + await this._removeMasterPassword(); + } else { + await this.changeMasterPassword(); + } + + this._initMasterPasswordUI(); + }, + + /** + * Displays the "remove master password" dialog to allow the user to remove + * the current master password. When the dialog is dismissed, master password + * UI is automatically updated. + */ + async _removeMasterPassword() { + var secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService( + Ci.nsIPKCS11ModuleDB + ); + if (secmodDB.isFIPSEnabled) { + let title = document.getElementById("fips-title").textContent; + let desc = document.getElementById("fips-desc").textContent; + Services.prompt.alert(window, title, desc); + this._initMasterPasswordUI(); + } else { + gSubDialog.open("chrome://mozapps/content/preferences/removemp.xhtml", { + closingCallback: this._initMasterPasswordUI.bind(this), + }); + } + }, + + /** + * Displays a dialog in which the primary password may be changed. + */ + async changeMasterPassword() { + // Require OS authentication before the user can set a Primary Password. + // OS reauthenticate functionality is not available on Linux yet (bug 1527745) + if ( + !LoginHelper.isPrimaryPasswordSet() && + OS_AUTH_ENABLED && + OSKeyStore.canReauth() + ) { + // Uses primary-password-os-auth-dialog-message-win and + // primary-password-os-auth-dialog-message-macosx via concatenation: + let messageId = + "primary-password-os-auth-dialog-message-" + AppConstants.platform; + let [messageText, captionText] = await document.l10n.formatMessages([ + { + id: messageId, + }, + { + id: "master-password-os-auth-dialog-caption", + }, + ]); + let win = Services.wm.getMostRecentBrowserWindow(); + let loggedIn = await OSKeyStore.ensureLoggedIn( + messageText.value, + captionText.value, + win, + false + ); + if (!loggedIn.authenticated) { + return; + } + } + + gSubDialog.open("chrome://mozapps/content/preferences/changemp.xhtml", { + features: "resizable=no", + closingCallback: this._initMasterPasswordUI.bind(this), + }); + }, + + /** + * Set up the initial state for the password generation UI. + * It will be hidden unless the .available pref is true + */ + _initPasswordGenerationUI() { + // we don't watch the .available pref for runtime changes + let prefValue = Services.prefs.getBoolPref( + PREF_PASSWORD_GENERATION_AVAILABLE, + false + ); + document.getElementById("generatePasswordsBox").hidden = !prefValue; + }, + + toggleRelayIntegration() { + const checkbox = document.getElementById("relayIntegration"); + if (checkbox.checked) { + FirefoxRelay.markAsAvailable(); + FirefoxRelayTelemetry.recordRelayPrefEvent("enabled"); + } else { + FirefoxRelay.markAsDisabled(); + FirefoxRelayTelemetry.recordRelayPrefEvent("disabled"); + } + }, + + _updateRelayIntegrationUI() { + document.getElementById("relayIntegrationBox").hidden = + !FirefoxRelay.isAvailable; + document.getElementById("relayIntegration").checked = + FirefoxRelay.isAvailable && !FirefoxRelay.isDisabled; + }, + + _initRelayIntegrationUI() { + document + .getElementById("relayIntegrationLearnMoreLink") + .setAttribute("href", FirefoxRelay.learnMoreUrl); + + setEventListener( + "relayIntegration", + "command", + gPrivacyPane.toggleRelayIntegration.bind(gPrivacyPane) + ); + Preferences.get("signon.firefoxRelay.feature").on( + "change", + gPrivacyPane._updateRelayIntegrationUI.bind(gPrivacyPane) + ); + + this._updateRelayIntegrationUI(); + }, + + /** + * Shows the sites where the user has saved passwords and the associated login + * information. + */ + showPasswords() { + let loginManager = window.windowGlobalChild.getActor("LoginManager"); + loginManager.sendAsyncMessage("PasswordManager:OpenPreferences", { + entryPoint: "preferences", + }); + }, + + /** + * Enables/disables dependent controls related to password saving + * When password saving is not enabled, we need to also disable the password generation checkbox + * The Exceptions button is used to configure sites where passwords are never saved. + */ + readSavePasswords() { + var prefValue = Preferences.get("signon.rememberSignons").value; + document.getElementById("passwordExceptions").disabled = !prefValue; + document.getElementById("generatePasswords").disabled = !prefValue; + document.getElementById("passwordAutofillCheckbox").disabled = !prefValue; + document.getElementById("relayIntegration").disabled = + !prefValue || Services.prefs.prefIsLocked("signon.firefoxRelay.feature"); + // don't override pref value in UI + return undefined; + }, + + /** + * Initalizes pref listeners for the password manager. + * + * This ensures that the user is always notified if an extension is controlling the password manager. + */ + initListenersForExtensionControllingPasswordManager() { + this._passwordManagerCheckbox = document.getElementById("savePasswords"); + this._disableExtensionButton = document.getElementById( + "disablePasswordManagerExtension" + ); + + this._disableExtensionButton.addEventListener( + "command", + makeDisableControllingExtension( + PREF_SETTING_TYPE, + PASSWORD_MANAGER_PREF_ID + ) + ); + + initListenersForPrefChange( + PREF_SETTING_TYPE, + PASSWORD_MANAGER_PREF_ID, + this._passwordManagerCheckbox + ); + }, + + /** + * Enables/disables the add-ons Exceptions button depending on whether + * or not add-on installation warnings are displayed. + */ + readWarnAddonInstall() { + var warn = Preferences.get("xpinstall.whitelist.required"); + var exceptions = document.getElementById("addonExceptions"); + + exceptions.disabled = !warn.value || warn.locked; + + // don't override the preference value + return undefined; + }, + + _initSafeBrowsing() { + let enableSafeBrowsing = document.getElementById("enableSafeBrowsing"); + let blockDownloads = document.getElementById("blockDownloads"); + let blockUncommonUnwanted = document.getElementById( + "blockUncommonUnwanted" + ); + + let safeBrowsingPhishingPref = Preferences.get( + "browser.safebrowsing.phishing.enabled" + ); + let safeBrowsingMalwarePref = Preferences.get( + "browser.safebrowsing.malware.enabled" + ); + + let blockDownloadsPref = Preferences.get( + "browser.safebrowsing.downloads.enabled" + ); + let malwareTable = Preferences.get("urlclassifier.malwareTable"); + + let blockUnwantedPref = Preferences.get( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted" + ); + let blockUncommonPref = Preferences.get( + "browser.safebrowsing.downloads.remote.block_uncommon" + ); + + enableSafeBrowsing.addEventListener("command", function () { + safeBrowsingPhishingPref.value = enableSafeBrowsing.checked; + safeBrowsingMalwarePref.value = enableSafeBrowsing.checked; + + blockDownloads.disabled = + !enableSafeBrowsing.checked || blockDownloadsPref.locked; + blockUncommonUnwanted.disabled = + !blockDownloads.checked || + !enableSafeBrowsing.checked || + blockUnwantedPref.locked || + blockUncommonPref.locked; + }); + + blockDownloads.addEventListener("command", function () { + blockDownloadsPref.value = blockDownloads.checked; + blockUncommonUnwanted.disabled = + !blockDownloads.checked || + blockUnwantedPref.locked || + blockUncommonPref.locked; + }); + + blockUncommonUnwanted.addEventListener("command", function () { + blockUnwantedPref.value = blockUncommonUnwanted.checked; + blockUncommonPref.value = blockUncommonUnwanted.checked; + + let malware = malwareTable.value + .split(",") + .filter( + x => + x !== "goog-unwanted-proto" && + x !== "goog-unwanted-shavar" && + x !== "moztest-unwanted-simple" + ); + + if (blockUncommonUnwanted.checked) { + if (malware.includes("goog-malware-shavar")) { + malware.push("goog-unwanted-shavar"); + } else { + malware.push("goog-unwanted-proto"); + } + + malware.push("moztest-unwanted-simple"); + } + + // sort alphabetically to keep the pref consistent + malware.sort(); + + malwareTable.value = malware.join(","); + + // Force an update after changing the malware table. + listManager.forceUpdates(malwareTable.value); + }); + + // set initial values + + enableSafeBrowsing.checked = + safeBrowsingPhishingPref.value && safeBrowsingMalwarePref.value; + if (!enableSafeBrowsing.checked) { + blockDownloads.setAttribute("disabled", "true"); + blockUncommonUnwanted.setAttribute("disabled", "true"); + } + + blockDownloads.checked = blockDownloadsPref.value; + if (!blockDownloadsPref.value) { + blockUncommonUnwanted.setAttribute("disabled", "true"); + } + blockUncommonUnwanted.checked = + blockUnwantedPref.value && blockUncommonPref.value; + + if (safeBrowsingPhishingPref.locked || safeBrowsingMalwarePref.locked) { + enableSafeBrowsing.disabled = true; + } + if (blockDownloadsPref.locked) { + blockDownloads.disabled = true; + } + if (blockUnwantedPref.locked || blockUncommonPref.locked) { + blockUncommonUnwanted.disabled = true; + } + }, + + /** + * Displays the exceptions lists for add-on installation warnings. + */ + showAddonExceptions() { + var params = this._addonParams; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + undefined, + params + ); + }, + + /** + * Parameters for the add-on install permissions dialog. + */ + _addonParams: { + blockVisible: false, + sessionVisible: false, + allowVisible: true, + prefilledHost: "", + permissionType: "install", + }, + + /** + * readEnableOCSP is used by the preferences UI to determine whether or not + * the checkbox for OCSP fetching should be checked (it returns true if it + * should be checked and false otherwise). The about:config preference + * "security.OCSP.enabled" is an integer rather than a boolean, so it can't be + * directly mapped from {true,false} to {checked,unchecked}. The possible + * values for "security.OCSP.enabled" are: + * 0: fetching is disabled + * 1: fetch for all certificates + * 2: fetch only for EV certificates + * Hence, if "security.OCSP.enabled" is non-zero, the checkbox should be + * checked. Otherwise, it should be unchecked. + */ + readEnableOCSP() { + var preference = Preferences.get("security.OCSP.enabled"); + // This is the case if the preference is the default value. + if (preference.value === undefined) { + return true; + } + return preference.value != 0; + }, + + /** + * writeEnableOCSP is used by the preferences UI to map the checked/unchecked + * state of the OCSP fetching checkbox to the value that the preference + * "security.OCSP.enabled" should be set to (it returns that value). See the + * readEnableOCSP documentation for more background. We unfortunately don't + * have enough information to map from {true,false} to all possible values for + * "security.OCSP.enabled", but a reasonable alternative is to map from + * {true,false} to {<the default value>,0}. That is, if the box is checked, + * "security.OCSP.enabled" will be set to whatever default it should be, given + * the platform and channel. If the box is unchecked, the preference will be + * set to 0. Obviously this won't work if the default is 0, so we will have to + * revisit this if we ever set it to 0. + */ + writeEnableOCSP() { + var checkbox = document.getElementById("enableOCSP"); + var defaults = Services.prefs.getDefaultBranch(null); + var defaultValue = defaults.getIntPref("security.OCSP.enabled"); + return checkbox.checked ? defaultValue : 0; + }, + + /** + * Displays the user's certificates and associated options. + */ + showCertificates() { + gSubDialog.open("chrome://pippki/content/certManager.xhtml"); + }, + + /** + * Displays a dialog from which the user can manage his security devices. + */ + showSecurityDevices() { + gSubDialog.open("chrome://pippki/content/device_manager.xhtml"); + }, + + /** + * Displays the learn more health report page when a user opts out of data collection. + */ + showDataDeletion() { + let url = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "telemetry-clientid"; + window.open(url, "_blank"); + }, + + initDataCollection() { + if ( + !AppConstants.MOZ_DATA_REPORTING && + !NimbusFeatures.majorRelease2022.getVariable( + "feltPrivacyShowPreferencesSection" + ) + ) { + // Nothing to control in the data collection section, remove it. + document.getElementById("dataCollectionCategory").remove(); + document.getElementById("dataCollectionGroup").remove(); + return; + } + + this._setupLearnMoreLink( + "toolkit.datacollection.infoURL", + "dataCollectionPrivacyNotice" + ); + this.initPrivacySegmentation(); + }, + + initSubmitCrashes() { + this._setupLearnMoreLink( + "toolkit.crashreporter.infoURL", + "crashReporterLearnMore" + ); + setEventListener("crashReporterLabel", "click", function (event) { + if (event.target.localName == "a") { + return; + } + const checkboxId = event.target.getAttribute("for"); + document.getElementById(checkboxId).click(); + }); + }, + + initPrivacySegmentation() { + // Section visibility + let section = document.getElementById("privacySegmentationSection"); + let updatePrivacySegmentationSectionVisibilityState = () => { + section.hidden = !NimbusFeatures.majorRelease2022.getVariable( + "feltPrivacyShowPreferencesSection" + ); + }; + + NimbusFeatures.majorRelease2022.onUpdate( + updatePrivacySegmentationSectionVisibilityState + ); + window.addEventListener("unload", () => { + NimbusFeatures.majorRelease2022.offUpdate( + updatePrivacySegmentationSectionVisibilityState + ); + }); + + updatePrivacySegmentationSectionVisibilityState(); + }, + + /** + * Set up or hide the Learn More links for various data collection options + */ + _setupLearnMoreLink(pref, element) { + // set up the Learn More link with the correct URL + let url = Services.urlFormatter.formatURLPref(pref); + let el = document.getElementById(element); + + if (url) { + el.setAttribute("href", url); + } else { + el.hidden = true; + } + }, + + /** + * Initialize the health report service reference and checkbox. + */ + initSubmitHealthReport() { + this._setupLearnMoreLink( + "datareporting.healthreport.infoURL", + "FHRLearnMore" + ); + + let checkbox = document.getElementById("submitHealthReportBox"); + + // Telemetry is only sending data if MOZ_TELEMETRY_REPORTING is defined. + // We still want to display the preferences panel if that's not the case, but + // we want it to be disabled and unchecked. + if ( + Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED) || + !AppConstants.MOZ_TELEMETRY_REPORTING + ) { + checkbox.setAttribute("disabled", "true"); + return; + } + + checkbox.checked = + Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED) && + AppConstants.MOZ_TELEMETRY_REPORTING; + }, + + /** + * Update the health report preference with state from checkbox. + */ + updateSubmitHealthReport() { + let checkbox = document.getElementById("submitHealthReportBox"); + let telemetryContainer = document.getElementById("telemetry-container"); + + Services.prefs.setBoolPref(PREF_UPLOAD_ENABLED, checkbox.checked); + telemetryContainer.hidden = checkbox.checked; + }, + + /** + * Initialize the opt-out-study preference checkbox into about:preferences and + * handles events coming from the UI for it. + */ + initOptOutStudyCheckbox(doc) { + // The checkbox should be disabled if any of the below are true. This + // prevents the user from changing the value in the box. + // + // * the policy forbids shield + // * Normandy is disabled + // + // The checkbox should match the value of the preference only if all of + // these are true. Otherwise, the checkbox should remain unchecked. This + // is because in these situations, Shield studies are always disabled, and + // so showing a checkbox would be confusing. + // + // * the policy allows Shield + // * Normandy is enabled + + const allowedByPolicy = Services.policies.isAllowed("Shield"); + const checkbox = document.getElementById("optOutStudiesEnabled"); + + if ( + allowedByPolicy && + Services.prefs.getBoolPref(PREF_NORMANDY_ENABLED, false) + ) { + if (Services.prefs.getBoolPref(PREF_OPT_OUT_STUDIES_ENABLED, false)) { + checkbox.setAttribute("checked", "true"); + } else { + checkbox.removeAttribute("checked"); + } + checkbox.setAttribute("preference", PREF_OPT_OUT_STUDIES_ENABLED); + checkbox.removeAttribute("disabled"); + } else { + checkbox.removeAttribute("preference"); + checkbox.removeAttribute("checked"); + checkbox.setAttribute("disabled", "true"); + } + }, + + initAddonRecommendationsCheckbox() { + // Setup the checkbox. + dataCollectionCheckboxHandler({ + checkbox: document.getElementById("addonRecommendationEnabled"), + pref: PREF_ADDON_RECOMMENDATIONS_ENABLED, + }); + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "sitedatamanager:updating-sites": + // While updating, we want to disable this section and display loading message until updated + this.toggleSiteData(false); + this.showSiteDataLoading(); + break; + + case "sitedatamanager:sites-updated": + this.toggleSiteData(true); + SiteDataManager.getTotalUsage().then( + this.updateTotalDataSizeLabel.bind(this) + ); + break; + case "network:trr-uri-changed": + case "network:trr-mode-changed": + case "network:trr-confirmation": + gPrivacyPane.updateDoHStatus(); + break; + } + }, +}; diff --git a/browser/components/preferences/search.inc.xhtml b/browser/components/preferences/search.inc.xhtml new file mode 100644 index 0000000000..b96af3c2f1 --- /dev/null +++ b/browser/components/preferences/search.inc.xhtml @@ -0,0 +1,118 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + + <script src="chrome://browser/content/preferences/search.js"/> + <html:template id="template-paneSearch"> + <hbox id="searchCategory" + class="subcategory" + hidden="true" + data-category="paneSearch"> + <html:h1 data-l10n-id="pane-search-title"/> + </hbox> + + <groupbox id="searchbarGroup" data-category="paneSearch" hidden="true"> + <label control="searchBarVisibleGroup"><html:h2 data-l10n-id="search-bar-header"/></label> + <radiogroup id="searchBarVisibleGroup" preference="browser.search.widget.inNavBar" align="start"> + <radio id="searchBarHiddenRadio" value="false" data-l10n-id="search-bar-hidden"/> + <image class="searchBarImage searchBarHiddenImage" role="presentation"/> + <checkbox id="searchShowSearchTermCheckbox" + data-l10n-id="search-show-search-term-option" + preference="browser.urlbar.showSearchTerms.enabled" + hidden="true" /> + <radio id="searchBarShownRadio" value="true" data-l10n-id="search-bar-shown"/> + <image class="searchBarImage searchBarShownImage" role="presentation"/> + </radiogroup> + </groupbox> + + <!-- Default Search Engine --> + <groupbox id="defaultEngineGroup" data-category="paneSearch" hidden="true"> + <label><html:h2 data-l10n-id="search-engine-default-header" /></label> + <description data-l10n-id="search-engine-default-desc-2" /> + <hbox> + <menulist id="defaultEngine"> + <menupopup/> + </menulist> + </hbox> + <checkbox id="browserSeparateDefaultEngine" + data-l10n-id="search-separate-default-engine" + hidden="true"/> + <vbox id="browserPrivateEngineSelection" class="indent" hidden="true"> + <description data-l10n-id="search-engine-default-private-desc-2" /> + <hbox> + <menulist id="defaultPrivateEngine"> + <menupopup/> + </menulist> + </hbox> + </vbox> + </groupbox> + + <groupbox id="searchSuggestionsGroup" data-category="paneSearch" hidden="true"> + <label><html:h2 data-l10n-id="search-suggestions-header" /></label> + <description id="searchSuggestionsDesc" + data-l10n-id="search-suggestions-desc" /> + + <checkbox id="suggestionsInSearchFieldsCheckbox" + data-l10n-id="search-suggestions-option" + preference="browser.search.suggest.enabled"/> + <vbox class="indent"> + <checkbox id="urlBarSuggestion" data-l10n-id="search-show-suggestions-url-bar-option" /> + <checkbox id="showSearchSuggestionsFirstCheckbox" + data-l10n-id="search-show-suggestions-above-history-option" + preference="browser.urlbar.showSearchSuggestionsFirst"/> + <checkbox id="showSearchSuggestionsPrivateWindows" + data-l10n-id="search-show-suggestions-private-windows"/> + <hbox id="urlBarSuggestionPermanentPBLabel" + align="center" class="indent"> + <label flex="1" data-l10n-id="search-suggestions-cant-show" /> + </hbox> + </vbox> + <label id="openLocationBarPrivacyPreferences" is="text-link" + data-l10n-id="suggestions-addressbar-settings-generic2"/> + </groupbox> + + <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch" hidden="true"> + <label><html:h2 data-l10n-id="search-one-click-header2" /></label> + <description data-l10n-id="search-one-click-desc" /> + + <tree id="engineList" flex="1" rows="11" hidecolumnpicker="true" editable="true" + seltype="single" allowunderflowscroll="true"> + <treechildren id="engineChildren" flex="1"/> + <treecols> + <treecol id="engineShown" type="checkbox" editable="true" sortable="false"/> + <treecol id="engineName" flex="1" data-l10n-id="search-choose-engine-column" sortable="false"/> + <treecol id="engineKeyword" flex="1" data-l10n-id="search-choose-keyword-column" sortable="false"/> + </treecols> + </tree> + + <hbox> + <button id="restoreDefaultSearchEngines" + is="highlightable-button" + data-l10n-id="search-restore-default" + /> + <spacer flex="1"/> + <button id="removeEngineButton" + is="highlightable-button" + class="searchEngineAction" + data-l10n-id="search-remove-engine" + disabled="true" + /> + <button id="addEngineButton" + is="highlightable-button" + class="searchEngineAction" + hidden="true" + data-l10n-id="search-add-engine" + search-l10n-ids=" + add-engine-button, + add-engine-name, + add-engine-alias, + add-engine-url, + add-engine-dialog.buttonlabelaccept, + " + /> + </hbox> + <hbox id="addEnginesBox" pack="start"> + <label id="addEngines" data-l10n-id="search-find-more-link" is="text-link"></label> + </hbox> + </groupbox> + </html:template> diff --git a/browser/components/preferences/search.js b/browser/components/preferences/search.js new file mode 100644 index 0000000000..52f49b77bb --- /dev/null +++ b/browser/components/preferences/search.js @@ -0,0 +1,1100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from extensionControlled.js */ +/* import-globals-from preferences.js */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs", +}); + +Preferences.addAll([ + { id: "browser.search.suggest.enabled", type: "bool" }, + { id: "browser.urlbar.suggest.searches", type: "bool" }, + { id: "browser.search.suggest.enabled.private", type: "bool" }, + { id: "browser.search.hiddenOneOffs", type: "unichar" }, + { id: "browser.search.widget.inNavBar", type: "bool" }, + { id: "browser.urlbar.showSearchSuggestionsFirst", type: "bool" }, + { id: "browser.urlbar.showSearchTerms.enabled", type: "bool" }, + { id: "browser.search.separatePrivateDefault", type: "bool" }, + { id: "browser.search.separatePrivateDefault.ui.enabled", type: "bool" }, +]); + +const ENGINE_FLAVOR = "text/x-moz-search-engine"; +const SEARCH_TYPE = "default_search"; +const SEARCH_KEY = "defaultSearch"; + +var gEngineView = null; + +var gSearchPane = { + init() { + gEngineView = new EngineView(new EngineStore()); + document.getElementById("engineList").view = gEngineView; + this.buildDefaultEngineDropDowns().catch(console.error); + + if ( + Services.policies && + !Services.policies.isAllowed("installSearchEngine") + ) { + document.getElementById("addEnginesBox").hidden = true; + } else { + let addEnginesLink = document.getElementById("addEngines"); + addEnginesLink.setAttribute("href", lazy.SearchUIUtils.searchEnginesURL); + } + + window.addEventListener("click", this); + window.addEventListener("command", this); + window.addEventListener("dragstart", this); + window.addEventListener("keypress", this); + window.addEventListener("select", this); + window.addEventListener("dblclick", this); + + Services.obs.addObserver(this, "browser-search-engine-modified"); + Services.obs.addObserver(this, "intl:app-locales-changed"); + window.addEventListener("unload", () => { + Services.obs.removeObserver(this, "browser-search-engine-modified"); + Services.obs.removeObserver(this, "intl:app-locales-changed"); + }); + + let suggestsPref = Preferences.get("browser.search.suggest.enabled"); + let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches"); + let privateSuggestsPref = Preferences.get( + "browser.search.suggest.enabled.private" + ); + let updateSuggestionCheckboxes = + this._updateSuggestionCheckboxes.bind(this); + suggestsPref.on("change", updateSuggestionCheckboxes); + urlbarSuggestsPref.on("change", updateSuggestionCheckboxes); + let urlbarSuggests = document.getElementById("urlBarSuggestion"); + urlbarSuggests.addEventListener("command", () => { + urlbarSuggestsPref.value = urlbarSuggests.checked; + }); + let privateWindowCheckbox = document.getElementById( + "showSearchSuggestionsPrivateWindows" + ); + privateWindowCheckbox.addEventListener("command", () => { + privateSuggestsPref.value = privateWindowCheckbox.checked; + }); + + setEventListener( + "browserSeparateDefaultEngine", + "command", + this._onBrowserSeparateDefaultEngineChange.bind(this) + ); + setEventListener( + "openLocationBarPrivacyPreferences", + "click", + function (event) { + if (event.button == 0) { + gotoPref("privacy-locationBar"); + } + } + ); + + this._initDefaultEngines(); + this._initShowSearchTermsCheckbox(); + this._updateSuggestionCheckboxes(); + this._showAddEngineButton(); + }, + + /** + * Initialize the default engine handling. This will hide the private default + * options if they are not enabled yet. + */ + _initDefaultEngines() { + this._separatePrivateDefaultEnabledPref = Preferences.get( + "browser.search.separatePrivateDefault.ui.enabled" + ); + + this._separatePrivateDefaultPref = Preferences.get( + "browser.search.separatePrivateDefault" + ); + + const checkbox = document.getElementById("browserSeparateDefaultEngine"); + checkbox.checked = !this._separatePrivateDefaultPref.value; + + this._updatePrivateEngineDisplayBoxes(); + + const listener = () => { + this._updatePrivateEngineDisplayBoxes(); + this.buildDefaultEngineDropDowns().catch(console.error); + }; + + this._separatePrivateDefaultEnabledPref.on("change", listener); + this._separatePrivateDefaultPref.on("change", listener); + }, + + _initShowSearchTermsCheckbox() { + let checkbox = document.getElementById("searchShowSearchTermCheckbox"); + + // Add Nimbus event to show/hide checkbox. + let onNimbus = () => { + checkbox.hidden = !UrlbarPrefs.get("showSearchTermsFeatureGate"); + }; + NimbusFeatures.urlbar.onUpdate(onNimbus); + + // Add observer of Search Bar preference as showSearchTerms + // can't be enabled/disabled while Search Bar is enabled. + let searchBarPref = Preferences.get("browser.search.widget.inNavBar"); + let updateCheckboxEnabled = () => { + checkbox.disabled = searchBarPref.value; + }; + searchBarPref.on("change", updateCheckboxEnabled); + + // Fire once to initialize. + onNimbus(); + updateCheckboxEnabled(); + + window.addEventListener("unload", () => { + NimbusFeatures.urlbar.offUpdate(onNimbus); + }); + }, + + _updatePrivateEngineDisplayBoxes() { + const separateEnabled = this._separatePrivateDefaultEnabledPref.value; + document.getElementById("browserSeparateDefaultEngine").hidden = + !separateEnabled; + + const separateDefault = this._separatePrivateDefaultPref.value; + + const vbox = document.getElementById("browserPrivateEngineSelection"); + vbox.hidden = !separateEnabled || !separateDefault; + }, + + _onBrowserSeparateDefaultEngineChange(event) { + this._separatePrivateDefaultPref.value = !event.target.checked; + }, + + _updateSuggestionCheckboxes() { + let suggestsPref = Preferences.get("browser.search.suggest.enabled"); + let permanentPB = Services.prefs.getBoolPref( + "browser.privatebrowsing.autostart" + ); + let urlbarSuggests = document.getElementById("urlBarSuggestion"); + let positionCheckbox = document.getElementById( + "showSearchSuggestionsFirstCheckbox" + ); + let privateWindowCheckbox = document.getElementById( + "showSearchSuggestionsPrivateWindows" + ); + + urlbarSuggests.disabled = !suggestsPref.value || permanentPB; + privateWindowCheckbox.disabled = !suggestsPref.value; + privateWindowCheckbox.checked = Preferences.get( + "browser.search.suggest.enabled.private" + ).value; + if (privateWindowCheckbox.disabled) { + privateWindowCheckbox.checked = false; + } + + let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches"); + urlbarSuggests.checked = urlbarSuggestsPref.value; + if (urlbarSuggests.disabled) { + urlbarSuggests.checked = false; + } + + if (urlbarSuggests.checked) { + positionCheckbox.disabled = false; + // Update the checked state of the show-suggestions-first checkbox. Note + // that this does *not* also update its pref, it only checks the box. + positionCheckbox.checked = Preferences.get( + positionCheckbox.getAttribute("preference") + ).value; + } else { + positionCheckbox.disabled = true; + positionCheckbox.checked = false; + } + + let permanentPBLabel = document.getElementById( + "urlBarSuggestionPermanentPBLabel" + ); + permanentPBLabel.hidden = urlbarSuggests.hidden || !permanentPB; + }, + + _showAddEngineButton() { + let aliasRefresh = Services.prefs.getBoolPref( + "browser.urlbar.update2.engineAliasRefresh", + false + ); + if (aliasRefresh) { + let addButton = document.getElementById("addEngineButton"); + addButton.hidden = false; + } + }, + + /** + * Builds the default and private engines drop down lists. This is called + * each time something affects the list of engines. + */ + async buildDefaultEngineDropDowns() { + await this._buildEngineDropDown( + document.getElementById("defaultEngine"), + ( + await Services.search.getDefault() + ).name, + false + ); + + if (this._separatePrivateDefaultEnabledPref.value) { + await this._buildEngineDropDown( + document.getElementById("defaultPrivateEngine"), + ( + await Services.search.getDefaultPrivate() + ).name, + true + ); + } + }, + + /** + * Builds a drop down menu of search engines. + * + * @param {DOMMenuList} list + * The menu list element to attach the list of engines. + * @param {string} currentEngine + * The name of the current default engine. + * @param {boolean} isPrivate + * True if we are dealing with the default engine for private mode. + */ + async _buildEngineDropDown(list, currentEngine, isPrivate) { + // If the current engine isn't in the list any more, select the first item. + let engines = gEngineView._engineStore._engines; + if (!engines.length) { + return; + } + if (!engines.some(e => e.name == currentEngine)) { + currentEngine = engines[0].name; + } + + // Now clean-up and rebuild the list. + list.removeAllItems(); + gEngineView._engineStore._engines.forEach(e => { + let item = list.appendItem(e.name); + item.setAttribute( + "class", + "menuitem-iconic searchengine-menuitem menuitem-with-favicon" + ); + if (e.iconURI) { + item.setAttribute("image", e.iconURI.spec); + } + item.engine = e; + if (e.name == currentEngine) { + list.selectedItem = item; + } + }); + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "dblclick": + if (aEvent.target.id == "engineChildren") { + let cell = aEvent.target.parentNode.getCellAt( + aEvent.clientX, + aEvent.clientY + ); + if (cell.col?.id == "engineKeyword") { + this.startEditingAlias(gEngineView.selectedIndex); + } + } + break; + case "click": + if ( + aEvent.target.id != "engineChildren" && + !aEvent.target.classList.contains("searchEngineAction") + ) { + let engineList = document.getElementById("engineList"); + // We don't want to toggle off selection while editing keyword + // so proceed only when the input field is hidden. + // We need to check that engineList.view is defined here + // because the "click" event listener is on <window> and the + // view might have been destroyed if the pane has been navigated + // away from. + if (engineList.inputField.hidden && engineList.view) { + let selection = engineList.view.selection; + if (selection?.count > 0) { + selection.toggleSelect(selection.currentIndex); + } + engineList.blur(); + } + } + break; + case "command": + switch (aEvent.target.id) { + case "": + if ( + aEvent.target.parentNode && + aEvent.target.parentNode.parentNode + ) { + if (aEvent.target.parentNode.parentNode.id == "defaultEngine") { + gSearchPane.setDefaultEngine(); + } else if ( + aEvent.target.parentNode.parentNode.id == "defaultPrivateEngine" + ) { + gSearchPane.setDefaultPrivateEngine(); + } + } + break; + case "restoreDefaultSearchEngines": + gSearchPane.onRestoreDefaults(); + break; + case "removeEngineButton": + Services.search.removeEngine( + gEngineView.selectedEngine.originalEngine + ); + break; + case "addEngineButton": + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/addEngine.xhtml", + { features: "resizable=no, modal=yes" } + ); + break; + } + break; + case "dragstart": + if (aEvent.target.id == "engineChildren") { + onDragEngineStart(aEvent); + } + break; + case "keypress": + if (aEvent.target.id == "engineList") { + gSearchPane.onTreeKeyPress(aEvent); + } + break; + case "select": + if (aEvent.target.id == "engineList") { + gSearchPane.onTreeSelect(); + } + break; + } + }, + + /** + * Handle when the app locale is changed. + */ + async appLocalesChanged() { + await document.l10n.ready; + await gEngineView.loadL10nNames(); + }, + + /** + * Update the default engine UI and engine tree view as appropriate when engine changes + * or locale changes occur. + * + * @param {Object} engine + * @param {string} data + */ + browserSearchEngineModified(engine, data) { + engine.QueryInterface(Ci.nsISearchEngine); + switch (data) { + case "engine-added": + gEngineView._engineStore.addEngine(engine); + gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1); + gSearchPane.buildDefaultEngineDropDowns(); + break; + case "engine-changed": + gSearchPane.buildDefaultEngineDropDowns(); + gEngineView._engineStore.updateEngine(engine); + gEngineView.invalidate(); + break; + case "engine-removed": + gSearchPane.remove(engine); + break; + case "engine-default": { + // If the user is going through the drop down using up/down keys, the + // dropdown may still be open (eg. on Windows) when engine-default is + // fired, so rebuilding the list unconditionally would get in the way. + let selectedEngine = + document.getElementById("defaultEngine").selectedItem.engine; + if (selectedEngine.name != engine.name) { + gSearchPane.buildDefaultEngineDropDowns(); + } + break; + } + case "engine-default-private": { + if ( + this._separatePrivateDefaultEnabledPref.value && + this._separatePrivateDefaultPref.value + ) { + // If the user is going through the drop down using up/down keys, the + // dropdown may still be open (eg. on Windows) when engine-default is + // fired, so rebuilding the list unconditionally would get in the way. + const selectedEngine = document.getElementById("defaultPrivateEngine") + .selectedItem.engine; + if (selectedEngine.name != engine.name) { + gSearchPane.buildDefaultEngineDropDowns(); + } + } + break; + } + } + }, + + /** + * nsIObserver implementation. + */ + observe(subject, topic, data) { + switch (topic) { + case "intl:app-locales-changed": { + this.appLocalesChanged(); + break; + } + case "browser-search-engine-modified": { + this.browserSearchEngineModified(subject, data); + break; + } + } + }, + + onTreeSelect() { + document.getElementById("removeEngineButton").disabled = + !gEngineView.isEngineSelectedAndRemovable(); + }, + + onTreeKeyPress(aEvent) { + let index = gEngineView.selectedIndex; + let tree = document.getElementById("engineList"); + if (tree.hasAttribute("editing")) { + return; + } + + if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) { + // Space toggles the checkbox. + let newValue = !gEngineView.getCellValue( + index, + tree.columns.getNamedColumn("engineShown") + ); + gEngineView.setCellValue( + index, + tree.columns.getFirstColumn(), + newValue.toString() + ); + // Prevent page from scrolling on the space key. + aEvent.preventDefault(); + } else { + let isMac = Services.appinfo.OS == "Darwin"; + if ( + (isMac && aEvent.keyCode == KeyEvent.DOM_VK_RETURN) || + (!isMac && aEvent.keyCode == KeyEvent.DOM_VK_F2) + ) { + this.startEditingAlias(index); + } else if ( + aEvent.keyCode == KeyEvent.DOM_VK_DELETE || + (isMac && + aEvent.shiftKey && + aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE && + gEngineView.isEngineSelectedAndRemovable()) + ) { + // Delete and Shift+Backspace (Mac) removes selected engine. + Services.search.removeEngine(gEngineView.selectedEngine.originalEngine); + } + } + }, + + startEditingAlias(index) { + // Local shortcut aliases can't be edited. + if (gEngineView._getLocalShortcut(index)) { + return; + } + + let tree = document.getElementById("engineList"); + let engine = gEngineView._engineStore.engines[index]; + tree.startEditing(index, tree.columns.getLastColumn()); + tree.inputField.value = engine.alias || ""; + tree.inputField.select(); + }, + + async onRestoreDefaults() { + let num = await gEngineView._engineStore.restoreDefaultEngines(); + gEngineView.rowCountChanged(0, num); + gEngineView.invalidate(); + }, + + showRestoreDefaults(aEnable) { + document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable; + }, + + remove(aEngine) { + let index = gEngineView._engineStore.removeEngine(aEngine); + if (!gEngineView.tree) { + // Only update the selection if it's visible in the UI. + return; + } + + gEngineView.rowCountChanged(index, -1); + gEngineView.invalidate(); + + gEngineView.selection.select(Math.min(index, gEngineView.rowCount - 1)); + gEngineView.ensureRowIsVisible(gEngineView.currentIndex); + + document.getElementById("engineList").focus(); + }, + + async editKeyword(aEngine, aNewKeyword) { + let keyword = aNewKeyword.trim(); + if (keyword) { + let eduplicate = false; + let dupName = ""; + + // Check for duplicates in Places keywords. + let bduplicate = !!(await PlacesUtils.keywords.fetch(keyword)); + + // Check for duplicates in changes we haven't committed yet + let engines = gEngineView._engineStore.engines; + let lc_keyword = keyword.toLocaleLowerCase(); + for (let engine of engines) { + if ( + engine.alias && + engine.alias.toLocaleLowerCase() == lc_keyword && + engine.name != aEngine.name + ) { + eduplicate = true; + dupName = engine.name; + break; + } + } + + // Notify the user if they have chosen an existing engine/bookmark keyword + if (eduplicate || bduplicate) { + let msgids = [{ id: "search-keyword-warning-title" }]; + if (eduplicate) { + msgids.push({ + id: "search-keyword-warning-engine", + args: { name: dupName }, + }); + } else { + msgids.push({ id: "search-keyword-warning-bookmark" }); + } + + let [dtitle, msg] = await document.l10n.formatValues(msgids); + + Services.prompt.alert(window, dtitle, msg); + return false; + } + } + + gEngineView._engineStore.changeEngine(aEngine, "alias", keyword); + gEngineView.invalidate(); + return true; + }, + + saveOneClickEnginesList() { + let hiddenList = []; + for (let engine of gEngineView._engineStore.engines) { + if (!engine.shown) { + hiddenList.push(engine.name); + } + } + Preferences.get("browser.search.hiddenOneOffs").value = + hiddenList.join(","); + }, + + async setDefaultEngine() { + await Services.search.setDefault( + document.getElementById("defaultEngine").selectedItem.engine, + Ci.nsISearchService.CHANGE_REASON_USER + ); + if (ExtensionSettingsStore.getSetting(SEARCH_TYPE, SEARCH_KEY) !== null) { + ExtensionSettingsStore.select( + ExtensionSettingsStore.SETTING_USER_SET, + SEARCH_TYPE, + SEARCH_KEY + ); + } + }, + + async setDefaultPrivateEngine() { + await Services.search.setDefaultPrivate( + document.getElementById("defaultPrivateEngine").selectedItem.engine, + Ci.nsISearchService.CHANGE_REASON_USER + ); + }, +}; + +function onDragEngineStart(event) { + var selectedIndex = gEngineView.selectedIndex; + + // Local shortcut rows can't be dragged or re-ordered. + if (gEngineView._getLocalShortcut(selectedIndex)) { + event.preventDefault(); + return; + } + + var tree = document.getElementById("engineList"); + let cell = tree.getCellAt(event.clientX, event.clientY); + if (selectedIndex >= 0 && !gEngineView.isCheckBox(cell.row, cell.col)) { + event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); + event.dataTransfer.effectAllowed = "move"; + } +} + +function EngineStore() { + let pref = Preferences.get("browser.search.hiddenOneOffs").value; + this.hiddenList = pref ? pref.split(",") : []; + + this._engines = []; + this._defaultEngines = []; + Promise.all([ + Services.search.getVisibleEngines(), + Services.search.getAppProvidedEngines(), + ]).then(([visibleEngines, defaultEngines]) => { + for (let engine of visibleEngines) { + this.addEngine(engine); + gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1); + } + this._defaultEngines = defaultEngines.map(this._cloneEngine, this); + gSearchPane.buildDefaultEngineDropDowns(); + + // check if we need to disable the restore defaults button + var someHidden = this._defaultEngines.some(e => e.hidden); + gSearchPane.showRestoreDefaults(someHidden); + }); +} +EngineStore.prototype = { + _engines: null, + _defaultEngines: null, + + get engines() { + return this._engines; + }, + set engines(val) { + this._engines = val; + }, + + _getIndexForEngine(aEngine) { + return this._engines.indexOf(aEngine); + }, + + _getEngineByName(aName) { + return this._engines.find(engine => engine.name == aName); + }, + + _cloneEngine(aEngine) { + var clonedObj = {}; + for (let i of ["id", "name", "alias", "iconURI", "hidden"]) { + clonedObj[i] = aEngine[i]; + } + clonedObj.originalEngine = aEngine; + clonedObj.shown = !this.hiddenList.includes(clonedObj.name); + return clonedObj; + }, + + // Callback for Array's some(). A thisObj must be passed to some() + _isSameEngine(aEngineClone) { + return aEngineClone.originalEngine.id == this.originalEngine.id; + }, + + addEngine(aEngine) { + this._engines.push(this._cloneEngine(aEngine)); + }, + + updateEngine(newEngine) { + let engineToUpdate = this._engines.findIndex( + e => e.originalEngine.id == newEngine.id + ); + if (engineToUpdate != -1) { + this.engines[engineToUpdate] = this._cloneEngine(newEngine); + } + }, + + moveEngine(aEngine, aNewIndex) { + if (aNewIndex < 0 || aNewIndex > this._engines.length - 1) { + throw new Error("ES_moveEngine: invalid aNewIndex!"); + } + var index = this._getIndexForEngine(aEngine); + if (index == -1) { + throw new Error("ES_moveEngine: invalid engine?"); + } + + if (index == aNewIndex) { + return Promise.resolve(); + } // nothing to do + + // Move the engine in our internal store + var removedEngine = this._engines.splice(index, 1)[0]; + this._engines.splice(aNewIndex, 0, removedEngine); + + return Services.search.moveEngine(aEngine.originalEngine, aNewIndex); + }, + + removeEngine(aEngine) { + if (this._engines.length == 1) { + throw new Error("Cannot remove last engine!"); + } + + let engineName = aEngine.name; + let index = this._engines.findIndex(element => element.name == engineName); + + if (index == -1) { + throw new Error("invalid engine?"); + } + + this._engines.splice(index, 1)[0]; + + if (aEngine.isAppProvided) { + gSearchPane.showRestoreDefaults(true); + } + gSearchPane.buildDefaultEngineDropDowns(); + return index; + }, + + async restoreDefaultEngines() { + var added = 0; + + for (var i = 0; i < this._defaultEngines.length; ++i) { + var e = this._defaultEngines[i]; + + // If the engine is already in the list, just move it. + if (this._engines.some(this._isSameEngine, e)) { + await this.moveEngine(this._getEngineByName(e.name), i); + } else { + // Otherwise, add it back to our internal store + + // The search service removes the alias when an engine is hidden, + // so clear any alias we may have cached before unhiding the engine. + e.alias = ""; + + this._engines.splice(i, 0, e); + let engine = e.originalEngine; + engine.hidden = false; + await Services.search.moveEngine(engine, i); + added++; + } + } + + // We can't do this as part of the loop above because the indices are + // used for moving engines. + let policyRemovedEngineNames = + Services.policies.getActivePolicies()?.SearchEngines?.Remove || []; + for (let engineName of policyRemovedEngineNames) { + let engine = Services.search.getEngineByName(engineName); + if (engine) { + try { + await Services.search.removeEngine(engine); + } catch (ex) { + // Engine might not exist + } + } + } + + Services.search.resetToAppDefaultEngine(); + gSearchPane.showRestoreDefaults(false); + gSearchPane.buildDefaultEngineDropDowns(); + return added; + }, + + changeEngine(aEngine, aProp, aNewValue) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) { + throw new Error("invalid engine?"); + } + + this._engines[index][aProp] = aNewValue; + aEngine.originalEngine[aProp] = aNewValue; + }, + + reloadIcons() { + this._engines.forEach(function (e) { + e.iconURI = e.originalEngine.iconURI; + }); + }, +}; + +function EngineView(aEngineStore) { + this._engineStore = aEngineStore; + + UrlbarPrefs.addObserver(this); + + this.loadL10nNames(); +} + +EngineView.prototype = { + _engineStore: null, + tree: null, + + loadL10nNames() { + // This maps local shortcut sources to their l10n names. The names are needed + // by getCellText. Getting the names is async but getCellText is not, so we + // cache them here to retrieve them syncronously in getCellText. + this._localShortcutL10nNames = new Map(); + return document.l10n + .formatValues( + UrlbarUtils.LOCAL_SEARCH_MODES.map(mode => { + let name = UrlbarUtils.getResultSourceName(mode.source); + return { id: `urlbar-search-mode-${name}` }; + }) + ) + .then(names => { + for (let { source } of UrlbarUtils.LOCAL_SEARCH_MODES) { + this._localShortcutL10nNames.set(source, names.shift()); + } + // Invalidate the tree now that we have the names in case getCellText was + // called before name retrieval finished. + this.invalidate(); + }); + }, + + get lastEngineIndex() { + return this._engineStore.engines.length - 1; + }, + + get selectedIndex() { + var seln = this.selection; + if (seln.getRangeCount() > 0) { + var min = {}; + seln.getRangeAt(0, min, {}); + return min.value; + } + return -1; + }, + + get selectedEngine() { + return this._engineStore.engines[this.selectedIndex]; + }, + + // Helpers + rowCountChanged(index, count) { + if (this.tree) { + this.tree.rowCountChanged(index, count); + } + }, + + invalidate() { + this.tree?.invalidate(); + }, + + ensureRowIsVisible(index) { + this.tree.ensureRowIsVisible(index); + }, + + getSourceIndexFromDrag(dataTransfer) { + return parseInt(dataTransfer.getData(ENGINE_FLAVOR)); + }, + + isCheckBox(index, column) { + return column.id == "engineShown"; + }, + + isEngineSelectedAndRemovable() { + let defaultEngine = Services.search.defaultEngine; + let defaultPrivateEngine = Services.search.defaultPrivateEngine; + // We don't allow the last remaining engine to be removed, thus the + // `this.lastEngineIndex != 0` check. + // We don't allow the default engine to be removed. + return ( + this.selectedIndex != -1 && + this.lastEngineIndex != 0 && + !this._getLocalShortcut(this.selectedIndex) && + this.selectedEngine.name != defaultEngine.name && + this.selectedEngine.name != defaultPrivateEngine.name + ); + }, + + /** + * Returns the local shortcut corresponding to a tree row, or null if the row + * is not a local shortcut. + * + * @param {number} index + * The tree row index. + * @returns {object} + * The local shortcut object or null if the row is not a local shortcut. + */ + _getLocalShortcut(index) { + let engineCount = this._engineStore.engines.length; + if (index < engineCount) { + return null; + } + return UrlbarUtils.LOCAL_SEARCH_MODES[index - engineCount]; + }, + + /** + * Called by UrlbarPrefs when a urlbar pref changes. + * + * @param {string} pref + * The name of the pref relative to the browser.urlbar branch. + */ + onPrefChanged(pref) { + // If one of the local shortcut prefs was toggled, toggle its row's + // checkbox. + let parts = pref.split("."); + if (parts[0] == "shortcuts" && parts[1] && parts.length == 2) { + this.invalidate(); + } + }, + + // nsITreeView + get rowCount() { + return ( + this._engineStore.engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length + ); + }, + + getImageSrc(index, column) { + if (column.id == "engineName") { + let shortcut = this._getLocalShortcut(index); + if (shortcut) { + return shortcut.icon; + } + + if (this._engineStore.engines[index].iconURI) { + return this._engineStore.engines[index].iconURI.spec; + } + + if (window.devicePixelRatio > 1) { + return "chrome://browser/skin/search-engine-placeholder@2x.png"; + } + return "chrome://browser/skin/search-engine-placeholder.png"; + } + + return ""; + }, + + getCellText(index, column) { + if (column.id == "engineName") { + let shortcut = this._getLocalShortcut(index); + if (shortcut) { + return this._localShortcutL10nNames.get(shortcut.source) || ""; + } + return this._engineStore.engines[index].name; + } else if (column.id == "engineKeyword") { + let shortcut = this._getLocalShortcut(index); + if (shortcut) { + return shortcut.restrict; + } + return this._engineStore.engines[index].originalEngine.aliases.join(", "); + } + return ""; + }, + + setTree(tree) { + this.tree = tree; + }, + + canDrop(targetIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + return ( + sourceIndex != -1 && + sourceIndex != targetIndex && + sourceIndex != targetIndex + orientation && + // Local shortcut rows can't be dragged or dropped on. + targetIndex < this._engineStore.engines.length + ); + }, + + async drop(dropIndex, orientation, dataTransfer) { + // Local shortcut rows can't be dragged or dropped on. This can sometimes + // be reached even though canDrop returns false for these rows. + if (this._engineStore.engines.length <= dropIndex) { + return; + } + + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + var sourceEngine = this._engineStore.engines[sourceIndex]; + + const nsITreeView = Ci.nsITreeView; + if (dropIndex > sourceIndex) { + if (orientation == nsITreeView.DROP_BEFORE) { + dropIndex--; + } + } else if (orientation == nsITreeView.DROP_AFTER) { + dropIndex++; + } + + await this._engineStore.moveEngine(sourceEngine, dropIndex); + gSearchPane.showRestoreDefaults(true); + gSearchPane.buildDefaultEngineDropDowns(); + + // Redraw, and adjust selection + this.invalidate(); + this.selection.select(dropIndex); + }, + + selection: null, + getRowProperties(index) { + return ""; + }, + getCellProperties(index, column) { + if (column.id == "engineName") { + // For local shortcut rows, return the result source name so we can style + // the icons in CSS. + let shortcut = this._getLocalShortcut(index); + if (shortcut) { + return UrlbarUtils.getResultSourceName(shortcut.source); + } + } + return ""; + }, + getColumnProperties(column) { + return ""; + }, + isContainer(index) { + return false; + }, + isContainerOpen(index) { + return false; + }, + isContainerEmpty(index) { + return false; + }, + isSeparator(index) { + return false; + }, + isSorted(index) { + return false; + }, + getParentIndex(index) { + return -1; + }, + hasNextSibling(parentIndex, index) { + return false; + }, + getLevel(index) { + return 0; + }, + getCellValue(index, column) { + if (column.id == "engineShown") { + let shortcut = this._getLocalShortcut(index); + if (shortcut) { + return UrlbarPrefs.get(shortcut.pref); + } + return this._engineStore.engines[index].shown; + } + return undefined; + }, + toggleOpenState(index) {}, + cycleHeader(column) {}, + selectionChanged() {}, + cycleCell(row, column) {}, + isEditable(index, column) { + return ( + column.id != "engineName" && + (column.id == "engineShown" || !this._getLocalShortcut(index)) + ); + }, + setCellValue(index, column, value) { + if (column.id == "engineShown") { + let shortcut = this._getLocalShortcut(index); + if (shortcut) { + UrlbarPrefs.set(shortcut.pref, value == "true"); + this.invalidate(); + return; + } + this._engineStore.engines[index].shown = value == "true"; + gEngineView.invalidate(); + gSearchPane.saveOneClickEnginesList(); + } + }, + setCellText(index, column, value) { + if (column.id == "engineKeyword") { + gSearchPane + .editKeyword(this._engineStore.engines[index], value) + .then(valid => { + if (!valid) { + gSearchPane.startEditingAlias(index); + } + }); + } + }, +}; diff --git a/browser/components/preferences/searchResults.inc.xhtml b/browser/components/preferences/searchResults.inc.xhtml new file mode 100644 index 0000000000..48dbeb9a54 --- /dev/null +++ b/browser/components/preferences/searchResults.inc.xhtml @@ -0,0 +1,25 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<hbox id="header-searchResults" + class="subcategory" + hidden="true" + data-hidden-from-search="true" + data-category="paneSearchResults"> + <html:h1 data-l10n-id="search-results-header"/> +</hbox> + +<groupbox id="no-results-message" + data-hidden-from-search="true" + data-category="paneSearchResults" + hidden="true"> + <vbox class="no-results-container"> + <label id="sorry-message" data-l10n-id="search-results-empty-message2"> + <html:span data-l10n-name="query" id="sorry-message-query"/> + </label> + <label id="need-help" data-l10n-id="search-results-help-link"> + <html:a is="moz-support-link" class="text-link" data-l10n-name="url" target="_blank" support-page="preferences"/> + </label> + </vbox> +</groupbox> diff --git a/browser/components/preferences/sync.inc.xhtml b/browser/components/preferences/sync.inc.xhtml new file mode 100644 index 0000000000..7df27eb994 --- /dev/null +++ b/browser/components/preferences/sync.inc.xhtml @@ -0,0 +1,245 @@ +# 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/. + +<!-- Sync panel --> + +<script src="chrome://browser/content/preferences/sync.js"/> +<html:template id="template-paneSync"> +<hbox id="firefoxAccountCategory" + class="subcategory" + hidden="true" + data-category="paneSync"> + <html:h1 data-l10n-id="pane-sync-title3"/> +</hbox> + +<deck id="weavePrefsDeck" data-category="paneSync" hidden="true" + data-hidden-from-search="true"> + <groupbox id="noFxaAccount"> + <hbox> + <vbox flex="1"> + <label id="noFxaCaption"><html:h2 data-l10n-id="sync-signedout-caption"/></label> + <description id="noFxaDescription" flex="1" data-l10n-id="sync-signedout-description2"/> + </vbox> + <vbox> + <image class="fxaSyncIllustration"/> + </vbox> + </hbox> + <hbox id="fxaNoLoginStatus" align="center" flex="1"> + <vbox flex="1"> + <hbox align="center" flex="1"> + <button id="noFxaSignIn" + is="highlightable-button" + class="accessory-button" + data-l10n-id="sync-signedout-account-signin3"/> + </hbox> + </vbox> + </hbox> + <label class="fxaMobilePromo" data-l10n-id="sync-mobile-promo"> + <html:img + src="chrome://browser/skin/logo-android.svg" + data-l10n-name="android-icon" + class="androidIcon"/> + <html:a + data-l10n-name="android-link" + class="fxaMobilePromo-android text-link" target="_blank"/> + <html:img + src="chrome://browser/skin/logo-ios.svg" + data-l10n-name="ios-icon" + class="iOSIcon"/> + <html:a + data-l10n-name="ios-link" + class="fxaMobilePromo-ios text-link" target="_blank"/> + </label> + </groupbox> + + <vbox id="hasFxaAccount"> + <hbox> + <vbox id="fxaContentWrapper" flex="1"> + <groupbox id="fxaGroup"> + <label class="search-header" hidden="true"><html:h2 data-l10n-id="pane-sync-title3"/></label> + + <deck id="fxaLoginStatus" flex="1"> + + <!-- logged in and verified and all is good --> + <hbox id="fxaLoginVerified" align="center" flex="1"> + <image id="openChangeProfileImage" + class="fxaProfileImage actionable" + role="button" + data-l10n-id="sync-profile-picture"/> + <vbox flex="1" pack="center"> + <hbox flex="1" align="baseline"> + <label id="fxaDisplayName" hidden="true"> + <html:h2 id="fxaDisplayNameHeading"/> + </label> + <label id="fxaEmailAddress" flex="1" crop="end"/> + <button id="fxaUnlinkButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="sync-sign-out"/> + </hbox> + <hbox> + <html:a id="verifiedManage" class="openLink" target="_blank" + data-l10n-id="sync-manage-account"/> + </hbox> + </vbox> + </hbox> + + <!-- logged in to an unverified account --> + <hbox id="fxaLoginUnverified"> + <vbox> + <image class="fxaProfileImage"/> + </vbox> + <vbox flex="1" pack="center"> + <hbox align="center"> + <image class="fxaLoginRejectedWarning"/> + <description flex="1" + class="l10nArgsEmailAddress" + data-l10n-id="sync-signedin-unverified" + data-l10n-args='{"email": ""}'/> + </hbox> + <hbox class="fxaAccountBoxButtons"> + <button id="verifyFxaAccount" + is="highlightable-button" + data-l10n-id="sync-resend-verification"/> + <button id="unverifiedUnlinkFxaAccount" + is="highlightable-button" + data-l10n-id="sync-remove-account"/> + </hbox> + </vbox> + </hbox> + + <!-- logged in locally but server rejected credentials --> + <hbox id="fxaLoginRejected"> + <vbox> + <image class="fxaProfileImage"/> + </vbox> + <vbox flex="1" pack="center"> + <hbox align="center"> + <image class="fxaLoginRejectedWarning"/> + <description flex="1" + class="l10nArgsEmailAddress" + data-l10n-id="sync-signedin-login-failure" + data-l10n-args='{"email": ""}'/> + </hbox> + <hbox class="fxaAccountBoxButtons"> + <button id="rejectReSignIn" + is="highlightable-button" + data-l10n-id="sync-sign-in"/> + <button id="rejectUnlinkFxaAccount" + is="highlightable-button" + data-l10n-id="sync-remove-account"/> + </hbox> + </vbox> + </hbox> + </deck> + </groupbox> + + <groupbox> + <label control="fxaSyncComputerName"> + <html:h2 data-l10n-id="sync-device-name-header"/> + </label> + <hbox id="fxaDeviceName"> + <html:input id="fxaSyncComputerName" type="text" disabled="true"/> + <button id="fxaChangeDeviceName" + is="highlightable-button" + class="needs-account-ready" + data-l10n-id="sync-device-name-change"/> + <button id="fxaCancelChangeDeviceName" + is="highlightable-button" + data-l10n-id="sync-device-name-cancel" + hidden="true"/> + <button id="fxaSaveChangeDeviceName" + is="highlightable-button" + data-l10n-id="sync-device-name-save" + hidden="true"/> + </hbox> + </groupbox> + + <groupbox> + <deck id="syncStatus" flex="1"> + <!-- sync not yet configured. --> + <vbox id="syncNotConfigured"> + <label> + <html:h2 data-l10n-id="prefs-syncing-off"/> + </label> + <hbox class="sync-group info-box-container"> + <vbox flex="1"> + <label data-l10n-id="prefs-sync-offer-setup-label2"/> + </vbox> + <vbox> + <button id="syncSetup" + is="highlightable-button" + class="accessory-button needs-account-ready" + data-l10n-id="prefs-sync-turn-on-syncing"/> + </vbox> + </hbox> + </vbox> + + <vbox id="syncConfigured"> + <hbox> + <html:h2 data-l10n-id="prefs-syncing-on"/> + <spacer flex="1"/> + <button id="syncNow" + class="accessory-button needs-account-ready" + data-l10n-id="prefs-sync-now" + data-l10n-attrs="labelnotsyncing, accesskeynotsyncing, labelsyncing"/> + </hbox> + <vbox class="sync-group info-box-container sync-configured"> + <label data-l10n-id="sync-syncing-across-devices-heading"/> + <html:div class="sync-engines-list"> + <html:div engine_preference="services.sync.engine.bookmarks"> + <image class="sync-engine-image sync-engine-bookmarks"/> + <label data-l10n-id="sync-currently-syncing-bookmarks"/> + </html:div> + <html:div engine_preference="services.sync.engine.history"> + <image class="sync-engine-image sync-engine-history"/> + <label data-l10n-id="sync-currently-syncing-history"/> + </html:div> + <html:div engine_preference="services.sync.engine.tabs"> + <image class="sync-engine-image sync-engine-tabs"/> + <label data-l10n-id="sync-currently-syncing-tabs"/> + </html:div> + <html:div engine_preference="services.sync.engine.passwords"> + <image class="sync-engine-image sync-engine-passwords"/> + <label data-l10n-id="sync-currently-syncing-logins-passwords"/> + </html:div> + <html:div engine_preference="services.sync.engine.addresses"> + <image class="sync-engine-image sync-engine-addresses"/> + <label data-l10n-id="sync-currently-syncing-addresses"/> + </html:div> + <html:div engine_preference="services.sync.engine.creditcards"> + <image class="sync-engine-image sync-engine-creditcards"/> + <label data-l10n-id="sync-currently-syncing-creditcards"/> + </html:div> + <html:div engine_preference="services.sync.engine.addons"> + <image class="sync-engine-image sync-engine-addons"/> + <label data-l10n-id="sync-currently-syncing-addons"/> + </html:div> + <html:div engine_preference="services.sync.engine.prefs"> + <image class="sync-engine-image sync-engine-prefs"/> + <label data-l10n-id="sync-currently-syncing-settings"/> + </html:div> + </html:div> + <hbox> + <button id="syncChangeOptions" + is="highlightable-button" + data-l10n-id="sync-change-options"/> + <spacer flex="1"/> + </hbox> + </vbox> + </vbox> + </deck> + </groupbox> + </vbox> + </hbox> + <vbox align="start"> + <html:a id="connect-another-device" + is="text-link" + class="fxaMobilePromo" + target="_blank" + data-l10n-id="sync-connect-another-device"/> + </vbox> + </vbox> +</deck> +</html:template> diff --git a/browser/components/preferences/sync.js b/browser/components/preferences/sync.js new file mode 100644 index 0000000000..80e89fd1c2 --- /dev/null +++ b/browser/components/preferences/sync.js @@ -0,0 +1,564 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from preferences.js */ + +XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () { + return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js"); +}); + +const FXA_PAGE_LOGGED_OUT = 0; +const FXA_PAGE_LOGGED_IN = 1; + +// Indexes into the "login status" deck. +// We are in a successful verified state - everything should work! +const FXA_LOGIN_VERIFIED = 0; +// We have logged in to an unverified account. +const FXA_LOGIN_UNVERIFIED = 1; +// We are logged in locally, but the server rejected our credentials. +const FXA_LOGIN_FAILED = 2; + +// Indexes into the "sync status" deck. +const SYNC_DISCONNECTED = 0; +const SYNC_CONNECTED = 1; + +var gSyncPane = { + get page() { + return document.getElementById("weavePrefsDeck").selectedIndex; + }, + + set page(val) { + document.getElementById("weavePrefsDeck").selectedIndex = val; + }, + + init() { + this._setupEventListeners(); + this.setupEnginesUI(); + + document + .getElementById("weavePrefsDeck") + .removeAttribute("data-hidden-from-search"); + + // If the Service hasn't finished initializing, wait for it. + let xps = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + + if (xps.ready) { + this._init(); + return; + } + + // it may take some time before all the promises we care about resolve, so + // pre-load what we can from synchronous sources. + this._showLoadPage(xps); + + let onUnload = function () { + window.removeEventListener("unload", onUnload); + try { + Services.obs.removeObserver(onReady, "weave:service:ready"); + } catch (e) {} + }; + + let onReady = () => { + Services.obs.removeObserver(onReady, "weave:service:ready"); + window.removeEventListener("unload", onUnload); + this._init(); + }; + + Services.obs.addObserver(onReady, "weave:service:ready"); + window.addEventListener("unload", onUnload); + + xps.ensureLoaded(); + }, + + _showLoadPage(xps) { + let maybeAcct = false; + let username = Services.prefs.getCharPref("services.sync.username", ""); + if (username) { + document.getElementById("fxaEmailAddress").textContent = username; + maybeAcct = true; + } + + let cachedComputerName = Services.prefs.getStringPref( + "identity.fxaccounts.account.device.name", + "" + ); + if (cachedComputerName) { + maybeAcct = true; + this._populateComputerName(cachedComputerName); + } + this.page = maybeAcct ? FXA_PAGE_LOGGED_IN : FXA_PAGE_LOGGED_OUT; + }, + + _init() { + Weave.Svc.Obs.add(UIState.ON_UPDATE, this.updateWeavePrefs, this); + + window.addEventListener("unload", () => { + Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.updateWeavePrefs, this); + }); + + FxAccounts.config + .promiseConnectDeviceURI(this._getEntryPoint()) + .then(connectURI => { + document + .getElementById("connect-another-device") + .setAttribute("href", connectURI); + }); + // Links for mobile devices. + for (let platform of ["android", "ios"]) { + let url = + Services.prefs.getCharPref(`identity.mobilepromo.${platform}`) + + "sync-preferences"; + for (let elt of document.querySelectorAll( + `.fxaMobilePromo-${platform}` + )) { + elt.setAttribute("href", url); + } + } + + this.updateWeavePrefs(); + + // Notify observers that the UI is now ready + Services.obs.notifyObservers(window, "sync-pane-loaded"); + + if ( + location.hash == "#sync" && + UIState.get().status == UIState.STATUS_SIGNED_IN + ) { + if (location.href.includes("action=pair")) { + gSyncPane.pairAnotherDevice(); + } else if (location.href.includes("action=choose-what-to-sync")) { + gSyncPane._chooseWhatToSync(false); + } + } + }, + + _toggleComputerNameControls(editMode) { + let textbox = document.getElementById("fxaSyncComputerName"); + textbox.disabled = !editMode; + document.getElementById("fxaChangeDeviceName").hidden = editMode; + document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode; + document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode; + }, + + _focusComputerNameTextbox() { + let textbox = document.getElementById("fxaSyncComputerName"); + let valLength = textbox.value.length; + textbox.focus(); + textbox.setSelectionRange(valLength, valLength); + }, + + _blurComputerNameTextbox() { + document.getElementById("fxaSyncComputerName").blur(); + }, + + _focusAfterComputerNameTextbox() { + // Focus the most appropriate element that's *not* the "computer name" box. + Services.focus.moveFocus( + window, + document.getElementById("fxaSyncComputerName"), + Services.focus.MOVEFOCUS_FORWARD, + 0 + ); + }, + + _updateComputerNameValue(save) { + if (save) { + let textbox = document.getElementById("fxaSyncComputerName"); + Weave.Service.clientsEngine.localName = textbox.value; + } + this._populateComputerName(Weave.Service.clientsEngine.localName); + }, + + _setupEventListeners() { + function setEventListener(aId, aEventType, aCallback) { + document + .getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gSyncPane)); + } + + setEventListener("openChangeProfileImage", "click", function (event) { + gSyncPane.openChangeProfileImage(event); + }); + setEventListener("openChangeProfileImage", "keypress", function (event) { + gSyncPane.openChangeProfileImage(event); + }); + setEventListener("fxaChangeDeviceName", "command", function () { + this._toggleComputerNameControls(true); + this._focusComputerNameTextbox(); + }); + setEventListener("fxaCancelChangeDeviceName", "command", function () { + // We explicitly blur the textbox because of bug 75324, then after + // changing the state of the buttons, force focus to whatever the focus + // manager thinks should be next (which on the mac, depends on an OSX + // keyboard access preference) + this._blurComputerNameTextbox(); + this._toggleComputerNameControls(false); + this._updateComputerNameValue(false); + this._focusAfterComputerNameTextbox(); + }); + setEventListener("fxaSaveChangeDeviceName", "command", function () { + // Work around bug 75324 - see above. + this._blurComputerNameTextbox(); + this._toggleComputerNameControls(false); + this._updateComputerNameValue(true); + this._focusAfterComputerNameTextbox(); + }); + setEventListener("noFxaSignIn", "command", function () { + gSyncPane.signIn(); + return false; + }); + setEventListener("fxaUnlinkButton", "command", function () { + gSyncPane.unlinkFirefoxAccount(true); + }); + setEventListener( + "verifyFxaAccount", + "command", + gSyncPane.verifyFirefoxAccount + ); + setEventListener("unverifiedUnlinkFxaAccount", "command", function () { + /* no warning as account can't have previously synced */ + gSyncPane.unlinkFirefoxAccount(false); + }); + setEventListener("rejectReSignIn", "command", gSyncPane.reSignIn); + setEventListener("rejectUnlinkFxaAccount", "command", function () { + gSyncPane.unlinkFirefoxAccount(true); + }); + setEventListener("fxaSyncComputerName", "keypress", function (e) { + if (e.keyCode == KeyEvent.DOM_VK_RETURN) { + document.getElementById("fxaSaveChangeDeviceName").click(); + } else if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) { + document.getElementById("fxaCancelChangeDeviceName").click(); + } + }); + setEventListener("syncSetup", "command", function () { + this._chooseWhatToSync(false); + }); + setEventListener("syncChangeOptions", "command", function () { + this._chooseWhatToSync(true); + }); + setEventListener("syncNow", "command", function () { + // syncing can take a little time to send the "started" notification, so + // pretend we already got it. + this._updateSyncNow(true); + Weave.Service.sync({ why: "aboutprefs" }); + }); + setEventListener("syncNow", "mouseover", function () { + const state = UIState.get(); + // If we are currently syncing, just set the tooltip to the same as the + // button label (ie, "Syncing...") + let tooltiptext = state.syncing + ? document.getElementById("syncNow").getAttribute("label") + : window.browsingContext.topChromeWindow.gSync.formatLastSyncDate( + state.lastSync + ); + document + .getElementById("syncNow") + .setAttribute("tooltiptext", tooltiptext); + }); + }, + + async _chooseWhatToSync(isAlreadySyncing) { + // Assuming another device is syncing and we're not, + // we update the engines selection so the correct + // checkboxes are pre-filed. + if (!isAlreadySyncing) { + try { + await Weave.Service.updateLocalEnginesState(); + } catch (err) { + console.error("Error updating the local engines state", err); + } + } + let params = {}; + if (isAlreadySyncing) { + // If we are already syncing then we also offer to disconnect. + params.disconnectFun = () => this.disconnectSync(); + } + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml", + { + closingCallback: event => { + if (!isAlreadySyncing && event.detail.button == "accept") { + // We weren't syncing but the user has accepted the dialog - so we + // want to start! + fxAccounts.telemetry + .recordConnection(["sync"], "ui") + .then(() => { + return Weave.Service.configure(); + }) + .catch(err => { + console.error("Failed to enable sync", err); + }); + } + }, + }, + params /* aParams */ + ); + }, + + _updateSyncNow(syncing) { + let butSyncNow = document.getElementById("syncNow"); + if (syncing) { + butSyncNow.setAttribute("label", butSyncNow.getAttribute("labelsyncing")); + butSyncNow.removeAttribute("accesskey"); + butSyncNow.disabled = true; + } else { + butSyncNow.setAttribute( + "label", + butSyncNow.getAttribute("labelnotsyncing") + ); + butSyncNow.setAttribute( + "accesskey", + butSyncNow.getAttribute("accesskeynotsyncing") + ); + butSyncNow.disabled = false; + } + }, + + updateWeavePrefs() { + let service = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + + let displayNameLabel = document.getElementById("fxaDisplayName"); + let fxaEmailAddressLabels = document.querySelectorAll( + ".l10nArgsEmailAddress" + ); + displayNameLabel.hidden = true; + + // while we determine the fxa status pre-load what we can. + this._showLoadPage(service); + + let state = UIState.get(); + if (state.status == UIState.STATUS_NOT_CONFIGURED) { + this.page = FXA_PAGE_LOGGED_OUT; + return; + } + this.page = FXA_PAGE_LOGGED_IN; + // We are logged in locally, but maybe we are in a state where the + // server rejected our credentials (eg, password changed on the server) + let fxaLoginStatus = document.getElementById("fxaLoginStatus"); + let syncReady = false; // Is sync able to actually sync? + // We need to check error states that need a re-authenticate to resolve + // themselves first. + if (state.status == UIState.STATUS_LOGIN_FAILED) { + fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED; + } else if (state.status == UIState.STATUS_NOT_VERIFIED) { + fxaLoginStatus.selectedIndex = FXA_LOGIN_UNVERIFIED; + } else { + // We must be golden (or in an error state we expect to magically + // resolve itself) + fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED; + syncReady = true; + } + fxaEmailAddressLabels.forEach(label => { + let l10nAttrs = document.l10n.getAttributes(label); + document.l10n.setAttributes(label, l10nAttrs.id, { email: state.email }); + }); + document.getElementById("fxaEmailAddress").textContent = state.email; + + this._populateComputerName(Weave.Service.clientsEngine.localName); + for (let elt of document.querySelectorAll(".needs-account-ready")) { + elt.disabled = !syncReady; + } + + // Clear the profile image (if any) of the previously logged in account. + document + .querySelector("#fxaLoginVerified > .fxaProfileImage") + .style.removeProperty("list-style-image"); + + if (state.displayName) { + fxaLoginStatus.setAttribute("hasName", true); + displayNameLabel.hidden = false; + document.getElementById("fxaDisplayNameHeading").textContent = + state.displayName; + } else { + fxaLoginStatus.removeAttribute("hasName"); + } + if (state.avatarURL && !state.avatarIsDefault) { + let bgImage = 'url("' + state.avatarURL + '")'; + let profileImageElement = document.querySelector( + "#fxaLoginVerified > .fxaProfileImage" + ); + profileImageElement.style.listStyleImage = bgImage; + + let img = new Image(); + img.onerror = () => { + // Clear the image if it has trouble loading. Since this callback is asynchronous + // we check to make sure the image is still the same before we clear it. + if (profileImageElement.style.listStyleImage === bgImage) { + profileImageElement.style.removeProperty("list-style-image"); + } + }; + img.src = state.avatarURL; + } + // The "manage account" link embeds the uid, so we need to update this + // if the account state changes. + FxAccounts.config + .promiseManageURI(this._getEntryPoint()) + .then(accountsManageURI => { + document + .getElementById("verifiedManage") + .setAttribute("href", accountsManageURI); + }); + // and the actual sync state. + let eltSyncStatus = document.getElementById("syncStatus"); + eltSyncStatus.hidden = !syncReady; + eltSyncStatus.selectedIndex = state.syncEnabled + ? SYNC_CONNECTED + : SYNC_DISCONNECTED; + this._updateSyncNow(state.syncing); + }, + + _getEntryPoint() { + let params = new URLSearchParams( + document.URL.split("#")[0].split("?")[1] || "" + ); + return params.get("entrypoint") || "preferences"; + }, + + openContentInBrowser(url, options) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + if (!win) { + openTrustedLinkIn(url, "tab"); + return; + } + win.switchToTabHavingURI(url, true, options); + }, + + // Replace the current tab with the specified URL. + replaceTabWithUrl(url) { + // Get the <browser> element hosting us. + let browser = window.docShell.chromeEventHandler; + // And tell it to load our URL. + browser.loadURI(Services.io.newURI(url), { + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( + {} + ), + }); + }, + + async signIn() { + if (!(await FxAccounts.canConnectAccount())) { + return; + } + const url = await FxAccounts.config.promiseConnectAccountURI( + this._getEntryPoint() + ); + this.replaceTabWithUrl(url); + }, + + async reSignIn() { + // There's a bit of an edge-case here - we might be forcing reauth when we've + // lost the FxA account data - in which case we'll not get a URL as the re-auth + // URL embeds account info and the server endpoint complains if we don't + // supply it - So we just use the regular "sign in" URL in that case. + if (!(await FxAccounts.canConnectAccount())) { + return; + } + + let entryPoint = this._getEntryPoint(); + const url = + (await FxAccounts.config.promiseForceSigninURI(entryPoint)) || + (await FxAccounts.config.promiseConnectAccountURI(entryPoint)); + this.replaceTabWithUrl(url); + }, + + clickOrSpaceOrEnterPressed(event) { + // Note: charCode is deprecated, but 'char' not yet implemented. + // Replace charCode with char when implemented, see Bug 680830 + return ( + (event.type == "click" && event.button == 0) || + (event.type == "keypress" && + (event.charCode == KeyEvent.DOM_VK_SPACE || + event.keyCode == KeyEvent.DOM_VK_RETURN)) + ); + }, + + openChangeProfileImage(event) { + if (this.clickOrSpaceOrEnterPressed(event)) { + FxAccounts.config + .promiseChangeAvatarURI(this._getEntryPoint()) + .then(url => { + this.openContentInBrowser(url, { + replaceQueryString: true, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + }); + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + }, + + async verifyFirefoxAccount() { + let titleL10nid, bodyL10nId; + try { + await fxAccounts.resendVerificationEmail(); + const { email } = await fxAccounts.getSignedInUser(); + titleL10nid = "sync-verification-sent-title"; + bodyL10nId = { id: "sync-verification-sent-body", args: { email } }; + } catch { + titleL10nid = "sync-verification-not-sent-title"; + bodyL10nId = "sync-verification-not-sent-body"; + } + const [title, body] = await document.l10n.formatValues([ + titleL10nid, + bodyL10nId, + ]); + new Notification(title, { body }); + }, + + // Disconnect the account, including everything linked. + unlinkFirefoxAccount(confirm) { + window.browsingContext.topChromeWindow.gSync.disconnect({ + confirm, + }); + }, + + // Disconnect sync, leaving the account connected. + disconnectSync() { + return window.browsingContext.topChromeWindow.gSync.disconnect({ + confirm: true, + disconnectAccount: false, + }); + }, + + pairAnotherDevice() { + gSubDialog.open( + "chrome://browser/content/preferences/fxaPairDevice.xhtml", + { features: "resizable=no" } + ); + }, + + _populateComputerName(value) { + let textbox = document.getElementById("fxaSyncComputerName"); + if (!textbox.hasAttribute("placeholder")) { + textbox.setAttribute( + "placeholder", + fxAccounts.device.getDefaultLocalName() + ); + } + textbox.value = value; + }, + + // arranges to dynamically show or hide sync engine name elements based on the + // preferences used for this engines. + setupEnginesUI() { + let observe = (elt, prefName) => { + elt.hidden = !Services.prefs.getBoolPref(prefName, false); + }; + + for (let elt of document.querySelectorAll("[engine_preference]")) { + let prefName = elt.getAttribute("engine_preference"); + let obs = observe.bind(null, elt, prefName); + obs(); + Services.prefs.addObserver(prefName, obs); + window.addEventListener("unload", () => { + Services.prefs.removeObserver(prefName, obs); + }); + } + }, +}; diff --git a/browser/components/preferences/tests/addons/pl-dictionary.xpi b/browser/components/preferences/tests/addons/pl-dictionary.xpi Binary files differnew file mode 100644 index 0000000000..cc4da1fa83 --- /dev/null +++ b/browser/components/preferences/tests/addons/pl-dictionary.xpi diff --git a/browser/components/preferences/tests/addons/set_homepage.xpi b/browser/components/preferences/tests/addons/set_homepage.xpi Binary files differnew file mode 100644 index 0000000000..9aff671021 --- /dev/null +++ b/browser/components/preferences/tests/addons/set_homepage.xpi diff --git a/browser/components/preferences/tests/addons/set_newtab.xpi b/browser/components/preferences/tests/addons/set_newtab.xpi Binary files differnew file mode 100644 index 0000000000..f11db0b6a8 --- /dev/null +++ b/browser/components/preferences/tests/addons/set_newtab.xpi diff --git a/browser/components/preferences/tests/browser.ini b/browser/components/preferences/tests/browser.ini new file mode 100644 index 0000000000..8203bceb90 --- /dev/null +++ b/browser/components/preferences/tests/browser.ini @@ -0,0 +1,152 @@ +[DEFAULT] +prefs = + extensions.formautofill.addresses.available='on' + extensions.formautofill.creditCards.available='on' + signon.management.page.os-auth.enabled=true +support-files = + head.js + privacypane_tests_perwindow.js + addons/pl-dictionary.xpi + addons/set_homepage.xpi + addons/set_newtab.xpi + +[browser_advanced_update.js] +skip-if = !updater +[browser_application_xml_handle_internally.js] +[browser_applications_selection.js] +[browser_basic_rebuild_fonts_test.js] +[browser_browser_languages_subdialog.js] +skip-if = + tsan + (!debug && os == 'win') # Bug 1518370 +[browser_bug1018066_resetScrollPosition.js] +[browser_bug1020245_openPreferences_to_paneContent.js] +[browser_bug1184989_prevent_scrolling_when_preferences_flipped.js] +skip-if = os == "mac" # 1664576 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +support-files = + browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml +[browser_bug1547020_lockedDownloadDir.js] +[browser_bug1579418.js] +[browser_bug410900.js] +[browser_bug731866.js] +[browser_bug795764_cachedisabled.js] +[browser_cert_export.js] +[browser_change_app_handler.js] +skip-if = os != "win" # Windows-specific handler application selection dialog +[browser_checkspelling.js] +[browser_connection.js] +[browser_connection_bug1445991.js] +[browser_connection_bug1505330.js] +skip-if = (verify && debug && (os == 'linux' || os == 'mac')) +[browser_connection_bug388287.js] +[browser_containers_name_input.js] +[browser_contentblocking.js] +skip-if = socketprocess_networking +[browser_contentblocking_categories.js] +[browser_contentblocking_standard_tcp_section.js] +[browser_cookie_exceptions_addRemove.js] +[browser_cookies_exceptions.js] +[browser_defaultbrowser_alwayscheck.js] +[browser_engines.js] +[browser_etp_exceptions_dialog.js] +[browser_experimental_features.js] +[browser_experimental_features_filter.js] +[browser_experimental_features_hidden_when_not_public.js] +skip-if = + os == "mac" && debug # Bug 1723854 + os == "linux" && os_version == "18.04" && debug # Bug 1723854 +[browser_experimental_features_resetall.js] +[browser_extension_controlled.js] +skip-if = + tsan + ccov && (os == 'linux' || os == 'win') # Linux: bug 1613530, Windows: bug 1437051 +[browser_filetype_dialog.js] +[browser_fluent.js] +[browser_homepage_default.js] +[browser_homepages_filter_aboutpreferences.js] +[browser_homepages_use_bookmark.js] +[browser_hometab_restore_defaults.js] +https_first_disabled = true +[browser_https_only_exceptions.js] +[browser_https_only_section.js] +[browser_ignore_invalid_capability.js] +[browser_languages_subdialog.js] +[browser_layersacceleration.js] +[browser_localSearchShortcuts.js] +[browser_moreFromMozilla.js] +[browser_moreFromMozilla_locales.js] +[browser_newtab_menu.js] +[browser_notifications_do_not_disturb.js] +[browser_open_download_preferences.js] +support-files = empty_pdf_file.pdf +[browser_open_migration_wizard.js] +[browser_password_management.js] +[browser_pdf_disabled.js] +[browser_performance.js] +[browser_performance_content_process_limit.js] +[browser_performance_e10srollout.js] +[browser_performance_non_e10s.js] +skip-if = true +[browser_permissions_checkPermissionsWereAdded.js] +[browser_permissions_dialog.js] +[browser_permissions_dialog_default_perm.js] +[browser_permissions_urlFieldHidden.js] +[browser_primaryPassword.js] +[browser_privacy_cookieBannerHandling.js] +[browser_privacy_dnsoverhttps.js] +[browser_privacy_firefoxSuggest.js] +[browser_privacy_passwordGenerationAndAutofill.js] +[browser_privacy_quickactions.js] +[browser_privacy_relayIntegration.js] +[browser_privacy_segmentation_pref.js] +[browser_privacy_syncDataClearing.js] +[browser_privacypane_2.js] +[browser_privacypane_3.js] +[browser_proxy_backup.js] +[browser_sanitizeOnShutdown_prefLocked.js] +[browser_searchChangedEngine.js] +[browser_searchDefaultEngine.js] +support-files = + engine1/manifest.json + engine2/manifest.json +[browser_searchFindMoreLink.js] +[browser_searchRestoreDefaults.js] +[browser_searchScroll.js] +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js +[browser_searchShowSuggestionsFirst.js] +[browser_search_no_results_change_category.js] +[browser_search_searchTerms.js] +[browser_search_subdialog_tooltip_saved_addresses.js] +[browser_search_subdialogs_within_preferences_1.js] +skip-if = tsan # Bug 1678829 +[browser_search_subdialogs_within_preferences_2.js] +[browser_search_subdialogs_within_preferences_3.js] +[browser_search_subdialogs_within_preferences_4.js] +[browser_search_subdialogs_within_preferences_5.js] +[browser_search_subdialogs_within_preferences_6.js] +[browser_search_subdialogs_within_preferences_7.js] +[browser_search_subdialogs_within_preferences_8.js] +[browser_search_subdialogs_within_preferences_site_data.js] +[browser_search_within_preferences_1.js] +skip-if = (os == 'win' && processor == "aarch64") # Bug 1536560 +[browser_search_within_preferences_2.js] +[browser_search_within_preferences_command.js] +[browser_searchsuggestions.js] +[browser_security-1.js] +[browser_security-2.js] +[browser_security-3.js] +[browser_site_login_exceptions.js] +[browser_site_login_exceptions_policy.js] +[browser_spotlight.js] +[browser_statePartitioning_PBM_strings.js] +[browser_statePartitioning_strings.js] +[browser_subdialogs.js] +support-files = + subdialog.xhtml + subdialog2.xhtml +[browser_sync_chooseWhatToSync.js] +[browser_sync_disabled.js] +[browser_sync_pairing.js] +[browser_warning_permanent_private_browsing.js] diff --git a/browser/components/preferences/tests/browser_advanced_update.js b/browser/components/preferences/tests/browser_advanced_update.js new file mode 100644 index 0000000000..95da7a1c7a --- /dev/null +++ b/browser/components/preferences/tests/browser_advanced_update.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cm = Components.manager; + +const uuidGenerator = Services.uuid; + +const mockUpdateManager = { + contractId: "@mozilla.org/updates/update-manager;1", + + _mockClassId: uuidGenerator.generateUUID(), + + _originalClassId: "", + + QueryInterface: ChromeUtils.generateQI(["nsIUpdateManager"]), + + createInstance(iiD) { + return this.QueryInterface(iiD); + }, + + register() { + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (!registrar.isCIDRegistered(this._mockClassId)) { + this._originalClassId = registrar.contractIDToCID(this.contractId); + registrar.registerFactory( + this._mockClassId, + "Unregister after testing", + this.contractId, + this + ); + } + }, + + unregister() { + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(this._mockClassId, this); + registrar.registerFactory(this._originalClassId, "", this.contractId, null); + }, + + getUpdateCount() { + return this._updates.length; + }, + + getUpdateAt(index) { + return this._updates[index]; + }, + + _updates: [ + { + name: "Firefox Developer Edition 49.0a2", + statusText: "The Update was successfully installed", + buildID: "20160728004010", + installDate: 1469763105156, + detailsURL: "https://www.mozilla.org/firefox/aurora/", + }, + { + name: "Firefox Developer Edition 43.0a2", + statusText: "The Update was successfully installed", + buildID: "20150929004011", + installDate: 1443585886224, + detailsURL: "https://www.mozilla.org/firefox/aurora/", + }, + { + name: "Firefox Developer Edition 42.0a2", + statusText: "The Update was successfully installed", + buildID: "20150920004018", + installDate: 1442818147544, + detailsURL: "https://www.mozilla.org/firefox/aurora/", + }, + ], +}; + +function formatInstallDate(sec) { + var date = new Date(sec); + const dtOptions = { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }; + return date.toLocaleString(undefined, dtOptions); +} + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let showBtn = doc.getElementById("showUpdateHistory"); + let dialogOverlay = content.gSubDialog._preloadDialog._overlay; + + // XXX: For unknown reasons, this mock cannot be loaded by + // XPCOMUtils.defineLazyServiceGetter() called in aboutDialog-appUpdater.js. + // It is registered here so that we could assert update history subdialog + // without stopping the preferences advanced pane from loading. + // See bug 1361929. + mockUpdateManager.register(); + + // Test the dialog window opens + ok( + BrowserTestUtils.is_hidden(dialogOverlay), + "The dialog should be invisible" + ); + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://mozapps/content/update/history.xhtml" + ); + showBtn.doCommand(); + await promiseSubDialogLoaded; + ok( + !BrowserTestUtils.is_hidden(dialogOverlay), + "The dialog should be visible" + ); + + let dialogFrame = dialogOverlay.querySelector(".dialogFrame"); + let frameDoc = dialogFrame.contentDocument; + let updates = frameDoc.querySelectorAll("richlistitem.update"); + + // Test the update history numbers are correct + is( + updates.length, + mockUpdateManager.getUpdateCount(), + "The update count is incorrect." + ); + + // Test the updates are displayed correctly + let update = null; + let updateData = null; + for (let i = 0; i < updates.length; ++i) { + update = updates[i]; + updateData = mockUpdateManager.getUpdateAt(i); + + let testcases = [ + { + selector: ".update-name", + id: "update-full-build-name", + args: { name: updateData.name, buildID: updateData.buildID }, + }, + { + selector: ".update-installedOn-label", + id: "update-installed-on", + args: { date: formatInstallDate(updateData.installDate) }, + }, + { + selector: ".update-status-label", + id: "update-status", + args: { status: updateData.statusText }, + }, + ]; + + for (let { selector, id, args } of testcases) { + const element = update.querySelector(selector); + const l10nAttrs = frameDoc.l10n.getAttributes(element); + Assert.deepEqual( + l10nAttrs, + { + id, + args, + }, + "Wrong " + id + ); + } + + if (update.detailsURL) { + is( + update.detailsURL, + update.querySelector(".text-link").href, + "Wrong detailsURL" + ); + } + } + + // Test the dialog window closes + let closeBtn = dialogOverlay.querySelector(".dialogClose"); + closeBtn.doCommand(); + ok( + BrowserTestUtils.is_hidden(dialogOverlay), + "The dialog should be invisible" + ); + + mockUpdateManager.unregister(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_application_xml_handle_internally.js b/browser/components/preferences/tests/browser_application_xml_handle_internally.js new file mode 100644 index 0000000000..edb4a4c0ec --- /dev/null +++ b/browser/components/preferences/tests/browser_application_xml_handle_internally.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const HandlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" +].getService(Ci.nsIHandlerService); + +const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + +// This test checks that application/xml has the handle internally option. +add_task(async function applicationXmlHandleInternally() { + const mimeInfo = MIMEService.getFromTypeAndExtension( + "application/xml", + "xml" + ); + HandlerService.store(mimeInfo); + registerCleanupFunction(() => { + HandlerService.remove(mimeInfo); + }); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First, find the application/xml item. + let xmlItem = container.querySelector("richlistitem[type='application/xml']"); + Assert.ok(xmlItem, "application/xml is present in handlersView"); + if (xmlItem) { + xmlItem.scrollIntoView({ block: "center" }); + xmlItem.closest("richlistbox").selectItem(xmlItem); + + // Open its menu + let list = xmlItem.querySelector(".actionsMenu"); + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(list, {}, win); + await popupShown; + + let handleInternallyItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']` + ); + + ok(!!handleInternallyItem, "handle internally is present"); + } + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_applications_selection.js b/browser/components/preferences/tests/browser_applications_selection.js new file mode 100644 index 0000000000..683ce76a89 --- /dev/null +++ b/browser/components/preferences/tests/browser_applications_selection.js @@ -0,0 +1,403 @@ +SimpleTest.requestCompleteLog(); +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +let gOldMailHandlers = []; +let gDummyHandlers = []; +let gOriginalPreferredMailHandler; +let gOriginalPreferredPDFHandler; + +registerCleanupFunction(function () { + function removeDummyHandlers(handlers) { + // Remove any of the dummy handlers we created. + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + if ( + gDummyHandlers.some( + h => + h.uriTemplate == + handlers.queryElementAt(i, Ci.nsIWebHandlerApp).uriTemplate + ) + ) { + handlers.removeElementAt(i); + } + } catch (ex) { + /* ignore non-web-app handlers */ + } + } + } + // Re-add the original protocol handlers: + let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + let mailHandlers = mailHandlerInfo.possibleApplicationHandlers; + for (let h of gOldMailHandlers) { + mailHandlers.appendElement(h); + } + removeDummyHandlers(mailHandlers); + mailHandlerInfo.preferredApplicationHandler = gOriginalPreferredMailHandler; + gHandlerService.store(mailHandlerInfo); + + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + removeDummyHandlers(pdfHandlerInfo.possibleApplicationHandlers); + pdfHandlerInfo.preferredApplicationHandler = gOriginalPreferredPDFHandler; + gHandlerService.store(pdfHandlerInfo); + + gBrowser.removeCurrentTab(); +}); + +function scrubMailtoHandlers(handlerInfo) { + // Remove extant web handlers because they have icons that + // we fetch from the web, which isn't allowed in tests. + let handlers = handlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + gOldMailHandlers.push(handler); + // If we get here, this is a web handler app. Remove it: + handlers.removeElementAt(i); + } catch (ex) {} + } +} + +add_setup(async function () { + // Create our dummy handlers + let handler1 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler1.name = "Handler 1"; + handler1.uriTemplate = "https://example.com/first/%s"; + + let handler2 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler2.name = "Handler 2"; + handler2.uriTemplate = "http://example.org/second/%s"; + gDummyHandlers.push(handler1, handler2); + + function substituteWebHandlers(handlerInfo) { + // Append the dummy handlers to replace them: + let handlers = handlerInfo.possibleApplicationHandlers; + handlers.appendElement(handler1); + handlers.appendElement(handler2); + gHandlerService.store(handlerInfo); + } + // Set up our mailto handler test infrastructure. + let mailtoHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + scrubMailtoHandlers(mailtoHandlerInfo); + gOriginalPreferredMailHandler = mailtoHandlerInfo.preferredApplicationHandler; + substituteWebHandlers(mailtoHandlerInfo); + + // Now do the same for pdf handler: + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + // PDF doesn't have built-in web handlers, so no need to scrub. + gOriginalPreferredPDFHandler = pdfHandlerInfo.preferredApplicationHandler; + substituteWebHandlers(pdfHandlerInfo); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + info("Preferences page opened on the general pane."); + + await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList; + info("Apps list loaded."); +}); + +async function selectStandardOptions(itemToUse) { + async function selectItemInPopup(item) { + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + // Synthesizing the mouse on the .actionsMenu menulist somehow just selects + // the top row. Probably something to do with the multiple layers of anon + // content - workaround by using the `.open` setter instead. + list.open = true; + await popupShown; + let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + if (typeof item == "function") { + item = item(); + } + popup.activateItem(item); + await popupHidden; + return item; + } + + let itemType = itemToUse.getAttribute("type"); + // Center the item. Center rather than top so it doesn't get blocked by + // the search header. + itemToUse.scrollIntoView({ block: "center" }); + itemToUse.closest("richlistbox").selectItem(itemToUse); + Assert.ok(itemToUse.selected, "Should be able to select our item."); + // Force reflow to make sure it's visible and the container dropdown isn't + // hidden. + itemToUse.getBoundingClientRect().top; + let list = itemToUse.querySelector(".actionsMenu"); + let popup = list.menupopup; + + // select one of our test cases: + let handlerItem = list.querySelector("menuitem[data-l10n-args*='Handler 1']"); + await selectItemInPopup(handlerItem); + let { preferredAction, alwaysAskBeforeHandling } = + HandlerServiceTestUtils.getHandlerInfo(itemType); + Assert.notEqual( + preferredAction, + Ci.nsIHandlerInfo.alwaysAsk, + "Should have selected something other than 'always ask' (" + itemType + ")" + ); + Assert.ok( + !alwaysAskBeforeHandling, + "Should have turned off asking before handling (" + itemType + ")" + ); + + // Test the alwaysAsk option + let alwaysAskItem = list.getElementsByAttribute( + "action", + Ci.nsIHandlerInfo.alwaysAsk + )[0]; + await selectItemInPopup(alwaysAskItem); + Assert.equal( + list.selectedItem, + alwaysAskItem, + "Should have selected always ask item (" + itemType + ")" + ); + alwaysAskBeforeHandling = + HandlerServiceTestUtils.getHandlerInfo(itemType).alwaysAskBeforeHandling; + Assert.ok( + alwaysAskBeforeHandling, + "Should have turned on asking before handling (" + itemType + ")" + ); + + let useDefaultItem = list.getElementsByAttribute( + "action", + Ci.nsIHandlerInfo.useSystemDefault + ); + useDefaultItem = useDefaultItem && useDefaultItem[0]; + if (useDefaultItem) { + await selectItemInPopup(useDefaultItem); + Assert.equal( + list.selectedItem, + useDefaultItem, + "Should have selected 'use default' item (" + itemType + ")" + ); + preferredAction = + HandlerServiceTestUtils.getHandlerInfo(itemType).preferredAction; + Assert.equal( + preferredAction, + Ci.nsIHandlerInfo.useSystemDefault, + "Should have selected 'use default' (" + itemType + ")" + ); + } else { + // Whether there's a "use default" item depends on the OS, so it's not + // possible to rely on it being the case or not. + info("No 'Use default' item, so not testing (" + itemType + ")"); + } + + // Select a web app item. + let webAppItems = Array.from( + popup.getElementsByAttribute("action", Ci.nsIHandlerInfo.useHelperApp) + ); + webAppItems = webAppItems.filter( + item => item.handlerApp instanceof Ci.nsIWebHandlerApp + ); + Assert.equal( + webAppItems.length, + 2, + "Should have 2 web application handler. (" + itemType + ")" + ); + Assert.notEqual( + webAppItems[0].label, + webAppItems[1].label, + "Should have 2 different web app handlers" + ); + let selectedItem = await selectItemInPopup(webAppItems[0]); + + // Test that the selected item label is the same as the label + // of the menu item. + let win = gBrowser.selectedBrowser.contentWindow; + await win.document.l10n.translateFragment(selectedItem); + await win.document.l10n.translateFragment(itemToUse); + Assert.equal( + selectedItem.label, + itemToUse.querySelector(".actionContainer label").value, + "Should have selected correct item (" + itemType + ")" + ); + let { preferredApplicationHandler } = + HandlerServiceTestUtils.getHandlerInfo(itemType); + preferredApplicationHandler.QueryInterface(Ci.nsIWebHandlerApp); + Assert.equal( + selectedItem.handlerApp.uriTemplate, + preferredApplicationHandler.uriTemplate, + "App should actually be selected in the backend. (" + itemType + ")" + ); + + // select the other web app item + selectedItem = await selectItemInPopup(webAppItems[1]); + + // Test that the selected item label is the same as the label + // of the menu item + await win.document.l10n.translateFragment(selectedItem); + await win.document.l10n.translateFragment(itemToUse); + Assert.equal( + selectedItem.label, + itemToUse.querySelector(".actionContainer label").value, + "Should have selected correct item (" + itemType + ")" + ); + preferredApplicationHandler = + HandlerServiceTestUtils.getHandlerInfo( + itemType + ).preferredApplicationHandler; + preferredApplicationHandler.QueryInterface(Ci.nsIWebHandlerApp); + Assert.equal( + selectedItem.handlerApp.uriTemplate, + preferredApplicationHandler.uriTemplate, + "App should actually be selected in the backend. (" + itemType + ")" + ); +} + +add_task(async function checkDropdownBehavior() { + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First check a protocol handler item. + let mailItem = container.querySelector("richlistitem[type='mailto']"); + Assert.ok(mailItem, "mailItem is present in handlersView."); + await selectStandardOptions(mailItem); + + // Then check a content menu item. + let pdfItem = container.querySelector("richlistitem[type='application/pdf']"); + Assert.ok(pdfItem, "pdfItem is present in handlersView."); + await selectStandardOptions(pdfItem); +}); + +add_task(async function sortingCheck() { + let win = gBrowser.selectedBrowser.contentWindow; + const handlerView = win.document.getElementById("handlersView"); + const typeColumn = win.document.getElementById("typeColumn"); + Assert.ok(typeColumn, "typeColumn is present in handlersView."); + + let expectedNumberOfItems = + handlerView.querySelectorAll("richlistitem").length; + + // Test default sorting + assertSortByType("ascending"); + + const oldDir = typeColumn.getAttribute("sortDirection"); + + // click on an item and sort again: + let itemToUse = handlerView.querySelector("richlistitem[type=mailto]"); + itemToUse.scrollIntoView({ block: "center" }); + itemToUse.closest("richlistbox").selectItem(itemToUse); + + // Test sorting on the type column + typeColumn.click(); + assertSortByType("descending"); + Assert.notEqual( + oldDir, + typeColumn.getAttribute("sortDirection"), + "Sort direction should change" + ); + + typeColumn.click(); + assertSortByType("ascending"); + + const actionColumn = win.document.getElementById("actionColumn"); + Assert.ok(actionColumn, "actionColumn is present in handlersView."); + + // Test sorting on the action column + const oldActionDir = actionColumn.getAttribute("sortDirection"); + actionColumn.click(); + assertSortByAction("ascending"); + Assert.notEqual( + oldActionDir, + actionColumn.getAttribute("sortDirection"), + "Sort direction should change" + ); + + actionColumn.click(); + assertSortByAction("descending"); + + // Restore the default sort order + typeColumn.click(); + assertSortByType("ascending"); + + function assertSortByAction(order) { + Assert.equal( + actionColumn.getAttribute("sortDirection"), + order, + `Sort direction should be ${order}` + ); + let siteItems = handlerView.getElementsByTagName("richlistitem"); + Assert.equal( + siteItems.length, + expectedNumberOfItems, + "Number of items should not change." + ); + for (let i = 0; i < siteItems.length - 1; ++i) { + let aType = siteItems[i].getAttribute("actionDescription").toLowerCase(); + let bType = siteItems[i + 1] + .getAttribute("actionDescription") + .toLowerCase(); + let result = 0; + if (aType > bType) { + result = 1; + } else if (bType > aType) { + result = -1; + } + if (order == "ascending") { + Assert.lessOrEqual( + result, + 0, + "Should sort applications in the ascending order by action" + ); + } else { + Assert.greaterOrEqual( + result, + 0, + "Should sort applications in the descending order by action" + ); + } + } + } + + function assertSortByType(order) { + Assert.equal( + typeColumn.getAttribute("sortDirection"), + order, + `Sort direction should be ${order}` + ); + + let siteItems = handlerView.getElementsByTagName("richlistitem"); + Assert.equal( + siteItems.length, + expectedNumberOfItems, + "Number of items should not change." + ); + for (let i = 0; i < siteItems.length - 1; ++i) { + let aType = siteItems[i].getAttribute("typeDescription").toLowerCase(); + let bType = siteItems[i + 1] + .getAttribute("typeDescription") + .toLowerCase(); + let result = 0; + if (aType > bType) { + result = 1; + } else if (bType > aType) { + result = -1; + } + if (order == "ascending") { + Assert.lessOrEqual( + result, + 0, + "Should sort applications in the ascending order by type" + ); + } else { + Assert.greaterOrEqual( + result, + 0, + "Should sort applications in the descending order by type" + ); + } + } + } +}); diff --git a/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js b/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js new file mode 100644 index 0000000000..bacda8a6b4 --- /dev/null +++ b/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js @@ -0,0 +1,235 @@ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + await gBrowser.contentWindow.gMainPane._selectDefaultLanguageGroupPromise; + await TestUtils.waitForCondition( + () => !gBrowser.contentWindow.Preferences.updateQueued + ); + + let doc = gBrowser.contentDocument; + let contentWindow = gBrowser.contentWindow; + var langGroup = Services.prefs.getComplexValue( + "font.language.group", + Ci.nsIPrefLocalizedString + ).data; + is( + contentWindow.Preferences.get("font.language.group").value, + langGroup, + "Language group should be set correctly." + ); + + let defaultFontType = Services.prefs.getCharPref("font.default." + langGroup); + let fontFamilyPref = "font.name." + defaultFontType + "." + langGroup; + let fontFamily = Services.prefs.getCharPref(fontFamilyPref); + let fontFamilyField = doc.getElementById("defaultFont"); + is(fontFamilyField.value, fontFamily, "Font family should be set correctly."); + + function dispatchMenuItemCommand(menuItem) { + const cmdEvent = doc.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + contentWindow, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + menuItem.dispatchEvent(cmdEvent); + } + + /** + * Return a promise that resolves when the fontFamilyPref changes. + * + * Font prefs are the only ones whose form controls set "delayprefsave", + * which delays the pref change when a user specifies a new value + * for the pref. Thus, in order to confirm that the pref gets changed + * when the test selects a new value in a font field, we need to await + * the change. Awaiting this function does so for fontFamilyPref. + */ + function fontFamilyPrefChanged() { + return new Promise(resolve => { + const observer = { + observe(aSubject, aTopic, aData) { + // Check for an exact match to avoid the ambiguity of nsIPrefBranch's + // prefix-matching algorithm for notifying pref observers. + if (aData == fontFamilyPref) { + Services.prefs.removeObserver(fontFamilyPref, observer); + resolve(); + } + }, + }; + Services.prefs.addObserver(fontFamilyPref, observer); + }); + } + + const menuItems = fontFamilyField.querySelectorAll("menuitem"); + ok(menuItems.length > 1, "There are multiple font menuitems."); + ok(menuItems[0].selected, "The first (default) font menuitem is selected."); + + dispatchMenuItemCommand(menuItems[1]); + ok(menuItems[1].selected, "The second font menuitem is selected."); + + await fontFamilyPrefChanged(); + fontFamily = Services.prefs.getCharPref(fontFamilyPref); + is(fontFamilyField.value, fontFamily, "The font family has been updated."); + + dispatchMenuItemCommand(menuItems[0]); + ok( + menuItems[0].selected, + "The first (default) font menuitem is selected again." + ); + + await fontFamilyPrefChanged(); + fontFamily = Services.prefs.getCharPref(fontFamilyPref); + is(fontFamilyField.value, fontFamily, "The font family has been updated."); + + let defaultFontSize = Services.prefs.getIntPref( + "font.size.variable." + langGroup + ); + let fontSizeField = doc.getElementById("defaultFontSize"); + is( + fontSizeField.value, + "" + defaultFontSize, + "Font size should be set correctly." + ); + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/fonts.xhtml" + ); + doc.getElementById("advancedFonts").click(); + let win = await promiseSubDialogLoaded; + doc = win.document; + + // Simulate a dumb font backend. + win.FontBuilder._enumerator = { + _list: ["MockedFont1", "MockedFont2", "MockedFont3"], + _defaultFont: null, + EnumerateFontsAsync(lang, type) { + return Promise.resolve(this._list); + }, + EnumerateAllFontsAsync() { + return Promise.resolve(this._list); + }, + getDefaultFont() { + return this._defaultFont; + }, + getStandardFamilyName(name) { + return name; + }, + }; + win.FontBuilder._allFonts = null; + win.FontBuilder._langGroupSupported = false; + + let langGroupElement = win.Preferences.get("font.language.group"); + let selectLangsField = doc.getElementById("selectLangs"); + let serifField = doc.getElementById("serif"); + let armenian = "x-armn"; + let western = "x-western"; + + // Await rebuilding of the font lists, which happens asynchronously in + // gFontsDialog._selectLanguageGroup. Testing code needs to call this + // function and await its resolution after changing langGroupElement's value + // (or doing anything else that triggers a call to _selectLanguageGroup). + function fontListsRebuilt() { + return win.gFontsDialog._selectLanguageGroupPromise; + } + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is(serifField.value, "", "Font family should not be set."); + + let armenianSerifElement = win.Preferences.get("font.name.serif.x-armn"); + + langGroupElement.value = western; + await fontListsRebuilt(); + selectLangsField.value = western; + + // Simulate a font backend supporting language-specific enumeration. + // NB: FontBuilder has cached the return value from EnumerateAllFonts(), + // so _allFonts will always have 3 elements regardless of subsequent + // _list changes. + win.FontBuilder._enumerator._list = ["MockedFont2"]; + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "", + "Font family should still be empty for indicating using 'default' font." + ); + + langGroupElement.value = western; + await fontListsRebuilt(); + selectLangsField.value = western; + + // Simulate a system that has no fonts for the specified language. + win.FontBuilder._enumerator._list = []; + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is(serifField.value, "", "Font family should not be set."); + + // Setting default font to "MockedFont3". Then, when serifField.value is + // empty, it should indicate using "MockedFont3" but it shouldn't be saved + // to "MockedFont3" in the pref. It should be resolved at runtime. + win.FontBuilder._enumerator._list = [ + "MockedFont1", + "MockedFont2", + "MockedFont3", + ]; + win.FontBuilder._enumerator._defaultFont = "MockedFont3"; + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "", + "Font family should be empty even if there is a default font." + ); + + armenianSerifElement.value = "MockedFont2"; + serifField.value = "MockedFont2"; + is( + serifField.value, + "MockedFont2", + 'Font family should be "MockedFont2" for now.' + ); + + langGroupElement.value = western; + await fontListsRebuilt(); + selectLangsField.value = western; + is(serifField.value, "", "Font family of other language should not be set."); + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "MockedFont2", + "Font family should not be changed even after switching the language." + ); + + // If MochedFont2 is removed from the system, the value should be treated + // as empty (i.e., 'default' font) after rebuilding the font list. + win.FontBuilder._enumerator._list = ["MockedFont1", "MockedFont3"]; + win.FontBuilder._enumerator._allFonts = ["MockedFont1", "MockedFont3"]; + serifField.removeAllItems(); // This will cause rebuilding the font list from available fonts. + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "", + "Font family should become empty due to the font uninstalled." + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_browser_languages_subdialog.js b/browser/components/preferences/tests/browser_browser_languages_subdialog.js new file mode 100644 index 0000000000..8b57bf08a8 --- /dev/null +++ b/browser/components/preferences/tests/browser_browser_languages_subdialog.js @@ -0,0 +1,1058 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const BROWSER_LANGUAGES_URL = + "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml"; +const DICTIONARY_ID_PL = "pl@dictionaries.addons.mozilla.org"; +const TELEMETRY_CATEGORY = "intl.ui.browserLanguage"; + +function langpackId(locale) { + return `langpack-${locale}@firefox.mozilla.org`; +} + +function getManifestData(locale, version = "2.0") { + return { + langpack_id: locale, + name: `${locale} Language Pack`, + description: `${locale} Language pack`, + languages: { + [locale]: { + chrome_resources: { + branding: `browser/chrome/${locale}/locale/branding/`, + }, + version: "1", + }, + }, + browser_specific_settings: { + gecko: { + id: langpackId(locale), + strict_min_version: AppConstants.MOZ_APP_VERSION, + strict_max_version: AppConstants.MOZ_APP_VERSION, + }, + }, + version, + manifest_version: 2, + sources: { + browser: { + base_path: "browser/", + }, + }, + author: "Mozilla", + }; +} + +let testLocales = ["fr", "pl", "he"]; +let testLangpacks; + +function createLangpack(locale, version) { + return AddonTestUtils.createTempXPIFile({ + "manifest.json": getManifestData(locale, version), + [`browser/${locale}/branding/brand.ftl`]: "-brand-short-name = Firefox", + }); +} + +function createTestLangpacks() { + if (!testLangpacks) { + testLangpacks = Promise.all( + testLocales.map(async locale => [locale, await createLangpack(locale)]) + ); + } + return testLangpacks; +} + +function createLocaleResult(target_locale, url) { + return { + guid: langpackId(target_locale), + type: "language", + target_locale, + current_compatible_version: { + files: [ + { + platform: "all", + url, + }, + ], + }, + }; +} + +async function createLanguageToolsFile() { + let langpacks = await createTestLangpacks(); + let results = langpacks.map(([locale, file]) => + createLocaleResult(locale, Services.io.newFileURI(file).spec) + ); + + let filename = "language-tools.json"; + let files = { [filename]: { results } }; + let tempdir = AddonTestUtils.tempDir.clone(); + let dir = await AddonTestUtils.promiseWriteFilesToDir(tempdir.path, files); + dir.append(filename); + + return dir; +} + +async function createDictionaryBrowseResults() { + let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); + let dictionaryPath = testDir + "/addons/pl-dictionary.xpi"; + let filename = "dictionaries.json"; + let response = { + page_size: 25, + page_count: 1, + count: 1, + results: [ + { + current_version: { + id: 1823648, + compatibility: { + firefox: { max: "9999", min: "4.0" }, + }, + files: [ + { + platform: "all", + url: dictionaryPath, + }, + ], + version: "1.0.20160228", + }, + default_locale: "pl", + description: "Polish spell-check", + guid: DICTIONARY_ID_PL, + name: "Polish Dictionary", + slug: "polish-spellchecker-dictionary", + status: "public", + summary: "Polish dictionary", + type: "dictionary", + }, + ], + }; + + let files = { [filename]: response }; + let dir = await AddonTestUtils.promiseWriteFilesToDir( + AddonTestUtils.tempDir.path, + files + ); + dir.append(filename); + + return dir; +} + +function assertLocaleOrder(list, locales) { + is( + list.itemCount, + locales.split(",").length, + "The right number of locales are selected" + ); + is( + Array.from(list.children) + .map(child => child.value) + .join(","), + locales, + "The selected locales are in order" + ); +} + +function assertAvailableLocales(list, locales) { + let items = Array.from(list.menupopup.children); + let listLocales = items.filter(item => item.value && item.value != "search"); + is( + listLocales.length, + locales.length, + "The right number of locales are available" + ); + is( + listLocales + .map(item => item.value) + .sort() + .join(","), + locales.sort().join(","), + "The available locales match" + ); + is(items[0].getAttribute("class"), "label-item", "The first row is a label"); +} + +function getDialogId(dialogDoc) { + return dialogDoc.ownerGlobal.arguments[0].telemetryId; +} + +function assertTelemetryRecorded(events) { + let snapshot = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + + // Make sure we got some data. + ok( + snapshot.parent && !!snapshot.parent.length, + "Got parent telemetry events in the snapshot" + ); + + // Only look at the related events after stripping the timestamp and category. + let relatedEvents = snapshot.parent + .filter(([timestamp, category]) => category == TELEMETRY_CATEGORY) + .map(relatedEvent => relatedEvent.slice(2, 6)); + + // Events are now an array of: method, object[, value[, extra]] as expected. + Assert.deepEqual(relatedEvents, events, "The events are recorded correctly"); +} + +async function selectLocale(localeCode, available, selected, dialogDoc) { + let [locale] = Array.from(available.menupopup.children).filter( + item => item.value == localeCode + ); + available.selectedItem = locale; + + // Get ready for the selected list to change. + let added = waitForMutation(selected, { childList: true }, target => + Array.from(target.children).some(el => el.value == localeCode) + ); + + // Add the locale. + dialogDoc.getElementById("add").doCommand(); + + // Wait for the list to update. + await added; +} + +async function openDialog(doc, search = false) { + let dialogLoaded = promiseLoadSubDialog(BROWSER_LANGUAGES_URL); + if (search) { + doc.getElementById("primaryBrowserLocaleSearch").doCommand(); + doc.getElementById("primaryBrowserLocale").menupopup.hidePopup(); + } else { + doc.getElementById("manageBrowserLanguagesButton").doCommand(); + } + let dialogWin = await dialogLoaded; + let dialogDoc = dialogWin.document; + return { + dialog: dialogDoc.getElementById("BrowserLanguagesDialog"), + dialogDoc, + available: dialogDoc.getElementById("availableLocales"), + selected: dialogDoc.getElementById("selectedLocales"), + }; +} + +add_task(async function testDisabledBrowserLanguages() { + let langpacksFile = await createLanguageToolsFile(); + let langpacksUrl = Services.io.newFileURI(langpacksFile).spec; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,pl,he,de"], + ["extensions.langpacks.signatures.required", false], + ["extensions.getAddons.langpacks.url", langpacksUrl], + ], + }); + + // Install an old pl langpack. + let oldLangpack = await createLangpack("pl", "1.0"); + await AddonTestUtils.promiseInstallFile(oldLangpack); + + // Install all the other available langpacks. + let pl; + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + if (locale == "pl") { + pl = await AddonManager.getAddonByID(langpackId("pl")); + // Disable pl so it's removed from selected. + await pl.disable(); + return pl; + } + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let { dialogDoc, available, selected } = await openDialog(doc); + + // pl is not selected since it's disabled. + is(pl.userDisabled, true, "pl is disabled"); + is(pl.version, "1.0", "pl is the old 1.0 version"); + assertLocaleOrder(selected, "en-US,he"); + + // Wait for the children menu to be populated. + await BrowserTestUtils.waitForCondition( + () => !!available.children.length, + "Children list populated" + ); + + // Only fr is enabled and not selected, so it's the only locale available. + assertAvailableLocales(available, ["fr"]); + + // Search for more languages. + available.menupopup.lastElementChild.doCommand(); + available.menupopup.hidePopup(); + await waitForMutation(available.menupopup, { childList: true }, target => + Array.from(available.menupopup.children).some( + locale => locale.value == "pl" + ) + ); + + // pl is now available since it is available remotely. + assertAvailableLocales(available, ["fr", "pl"]); + + let installId = null; + AddonTestUtils.promiseInstallEvent("onInstallEnded").then(([install]) => { + installId = install.installId; + }); + + // Add pl. + await selectLocale("pl", available, selected, dialogDoc); + assertLocaleOrder(selected, "pl,en-US,he"); + + // Find pl again since it's been upgraded. + pl = await AddonManager.getAddonByID(langpackId("pl")); + is(pl.userDisabled, false, "pl is now enabled"); + is(pl.version, "2.0", "pl is upgraded to version 2.0"); + + let dialogId = getDialogId(dialogDoc); + ok(dialogId, "There's a dialogId"); + ok(installId, "There's an installId"); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + ["manage", "main", dialogId], + ["search", "dialog", dialogId], + ["add", "dialog", dialogId, { installId }], + + // Cancel is recorded when the tab is closed. + ["cancel", "dialog", dialogId], + ]); +}); + +add_task(async function testReorderingBrowserLanguages() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,pl,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Install all the available langpacks. + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // Open the dialog. + let { dialog, dialogDoc, selected } = await openDialog(doc); + let firstDialogId = getDialogId(dialogDoc); + + // The initial order is set by the pref, filtered by available. + assertLocaleOrder(selected, "en-US,pl,he"); + + // Moving pl down changes the order. + selected.selectedItem = selected.querySelector("[value='pl']"); + dialogDoc.getElementById("down").doCommand(); + assertLocaleOrder(selected, "en-US,he,pl"); + + // Accepting the change shows the confirm message bar. + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + + // The message bar uses async `formatValues` and that may resolve + // after the dialog is closed. + await BrowserTestUtils.waitForMutationCondition( + messageBar, + { attributes: true }, + () => !messageBar.hidden + ); + is( + messageBar.querySelector("button").getAttribute("locales"), + "en-US,he,pl", + "The locales are set on the message bar button" + ); + + // Open the dialog again. + let newDialog = await openDialog(doc); + dialog = newDialog.dialog; + dialogDoc = newDialog.dialogDoc; + let secondDialogId = getDialogId(dialogDoc); + selected = newDialog.selected; + + // The initial order comes from the previous settings. + assertLocaleOrder(selected, "en-US,he,pl"); + + // Select pl in the list. + selected.selectedItem = selected.querySelector("[value='pl']"); + // Move pl back up. + dialogDoc.getElementById("up").doCommand(); + assertLocaleOrder(selected, "en-US,pl,he"); + + // Accepting the change hides the confirm message bar. + dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + is(messageBar.hidden, true, "The message bar is hidden again"); + + ok(firstDialogId, "There was an id on the first dialog"); + ok(secondDialogId, "There was an id on the second dialog"); + ok(firstDialogId != secondDialogId, "The dialog ids are different"); + ok( + parseInt(firstDialogId) < parseInt(secondDialogId), + "The second dialog id is larger than the first" + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + ["manage", "main", firstDialogId], + ["reorder", "dialog", firstDialogId], + ["accept", "dialog", firstDialogId], + ["manage", "main", secondDialogId], + ["reorder", "dialog", secondDialogId], + ["accept", "dialog", secondDialogId], + ]); +}); + +add_task(async function testAddAndRemoveSelectedLanguages() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // Open the dialog. + let { dialog, dialogDoc, available, selected } = await openDialog(doc); + let dialogId = getDialogId(dialogDoc); + + // loadLocalesFromAMO is async but `initAvailableLocales` doesn't wait + // for it to be resolved, so we have to wait for the list to be populated + // before we test for its values. + await BrowserTestUtils.waitForMutationCondition( + available.menupopup, + { attributes: true, childList: true }, + () => { + let listLocales = Array.from(available.menupopup.children).filter( + item => item.value && item.value != "search" + ); + return listLocales.length == 3; + } + ); + // The initial order is set by the pref. + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "pl", "he"]); + + // Add pl and fr to selected. + await selectLocale("pl", available, selected, dialogDoc); + await selectLocale("fr", available, selected, dialogDoc); + + assertLocaleOrder(selected, "fr,pl,en-US"); + assertAvailableLocales(available, ["he"]); + + // Remove pl and fr from selected. + dialogDoc.getElementById("remove").doCommand(); + dialogDoc.getElementById("remove").doCommand(); + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "pl", "he"]); + + // Add he to selected. + await selectLocale("he", available, selected, dialogDoc); + assertLocaleOrder(selected, "he,en-US"); + assertAvailableLocales(available, ["pl", "fr"]); + + // Accepting the change shows the confirm message bar. + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + + await waitForMutation( + messageBar, + { attributes: true, attributeFilter: ["hidden"] }, + target => !target.hidden + ); + + is(messageBar.hidden, false, "The message bar is now visible"); + is( + messageBar.querySelector("button").getAttribute("locales"), + "he,en-US", + "The locales are set on the message bar button" + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + ["manage", "main", dialogId], + + // Install id is not recorded since it was already installed. + ["add", "dialog", dialogId], + ["add", "dialog", dialogId], + + ["remove", "dialog", dialogId], + ["remove", "dialog", dialogId], + + ["add", "dialog", dialogId], + ["accept", "dialog", dialogId], + ]); +}); + +add_task(async function testInstallFromAMO() { + let langpacks = await AddonManager.getAddonsByTypes(["locale"]); + is(langpacks.length, 0, "There are no langpacks installed"); + + let langpacksFile = await createLanguageToolsFile(); + let langpacksUrl = Services.io.newFileURI(langpacksFile).spec; + let dictionaryBrowseFile = await createDictionaryBrowseResults(); + let browseApiEndpoint = Services.io.newFileURI(dictionaryBrowseFile).spec; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US"], + ["extensions.getAddons.langpacks.url", langpacksUrl], + ["extensions.langpacks.signatures.required", false], + ["extensions.getAddons.get.url", browseApiEndpoint], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // Verify only en-US is listed on the main pane. + let getMainPaneLocales = () => { + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + return availableLocales + .map(item => item.value) + .sort() + .join(","); + }; + is(getMainPaneLocales(), "en-US,search", "Only en-US installed to start"); + + // Open the dialog. + let { dialog, dialogDoc, available, selected } = await openDialog(doc, true); + let firstDialogId = getDialogId(dialogDoc); + + // Make sure the message bar is still hidden. + is( + messageBar.hidden, + true, + "The message bar is still hidden after searching" + ); + + if (available.itemCount == 1) { + await waitForMutation( + available.menupopup, + { childList: true }, + target => available.itemCount > 1 + ); + } + + // The initial order is set by the pref. + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "he", "pl"]); + is( + Services.locale.availableLocales.join(","), + "en-US", + "There is only one installed locale" + ); + + // Verify that there are no extra dictionaries. + let dicts = await AddonManager.getAddonsByTypes(["dictionary"]); + is(dicts.length, 0, "There are no installed dictionaries"); + + let installId = null; + AddonTestUtils.promiseInstallEvent("onInstallEnded").then(([install]) => { + installId = install.installId; + }); + + // Add Polish, this will install the langpack. + await selectLocale("pl", available, selected, dialogDoc); + + ok(installId, "We got an installId for the langpack installation"); + + let langpack = await AddonManager.getAddonByID(langpackId("pl")); + Assert.deepEqual( + langpack.installTelemetryInfo, + { source: "about:preferences" }, + "The source is set to preferences" + ); + + // Verify the list is correct. + assertLocaleOrder(selected, "pl,en-US"); + assertAvailableLocales(available, ["fr", "he"]); + is( + Services.locale.availableLocales.sort().join(","), + "en-US,pl", + "Polish is now installed" + ); + + await BrowserTestUtils.waitForCondition(async () => { + let newDicts = await AddonManager.getAddonsByTypes(["dictionary"]); + let done = !!newDicts.length; + + if (done) { + is( + newDicts[0].id, + DICTIONARY_ID_PL, + "The polish dictionary was installed" + ); + } + + return done; + }); + + // Move pl down the list, which prevents an error since it isn't valid. + dialogDoc.getElementById("down").doCommand(); + assertLocaleOrder(selected, "en-US,pl"); + + // Test that disabling the langpack removes it from the list. + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + + // Verify pl is now available to select. + is(getMainPaneLocales(), "en-US,pl,search", "en-US and pl now available"); + + // Disable the Polish langpack. + langpack = await AddonManager.getAddonByID("langpack-pl@firefox.mozilla.org"); + await langpack.disable(); + + ({ dialogDoc, available, selected } = await openDialog(doc, true)); + let secondDialogId = getDialogId(dialogDoc); + + // Wait for the available langpacks to load. + if (available.itemCount == 1) { + await waitForMutation( + available.menupopup, + { childList: true }, + target => available.itemCount > 1 + ); + } + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "he", "pl"]); + + // Uninstall the langpack and dictionary. + let installs = await AddonManager.getAddonsByTypes(["locale", "dictionary"]); + is(installs.length, 2, "There is one langpack and one dictionary installed"); + await Promise.all(installs.map(item => item.uninstall())); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + // First dialog installs a locale and accepts. + ["search", "main", firstDialogId], + // It has an installId since it was downloaded. + ["add", "dialog", firstDialogId, { installId }], + // It got moved down to avoid errors with finding translations. + ["reorder", "dialog", firstDialogId], + ["accept", "dialog", firstDialogId], + + // The second dialog just checks the state and is closed with the tab. + ["search", "main", secondDialogId], + ["cancel", "dialog", secondDialogId], + ]); +}); + +let hasSearchOption = popup => + Array.from(popup.children).some(el => el.value == "search"); + +add_task(async function testDownloadEnabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let defaultMenulist = doc.getElementById("primaryBrowserLocale"); + ok( + hasSearchOption(defaultMenulist.menupopup), + "There's a search option in the General pane" + ); + + let { available } = await openDialog(doc, false); + ok( + hasSearchOption(available.menupopup), + "There's a search option in the dialog" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testDownloadDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", false], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let defaultMenulist = doc.getElementById("primaryBrowserLocale"); + ok( + !hasSearchOption(defaultMenulist.menupopup), + "There's no search option in the General pane" + ); + + let { available } = await openDialog(doc, false); + ok( + !hasSearchOption(available.menupopup), + "There's no search option in the dialog" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testReorderMainPane() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", false], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + let availableCodes = availableLocales + .map(item => item.value) + .sort() + .join(","); + is( + availableCodes, + "en-US,fr,he,pl", + "All of the available locales are listed" + ); + + is(available.selectedItem.value, "en-US", "English is selected"); + + let hebrew = availableLocales.find(item => item.value == "he"); + hebrew.click(); + available.menupopup.hidePopup(); + + await BrowserTestUtils.waitForCondition( + () => !messageBar.hidden, + "Wait for message bar to show" + ); + + is(messageBar.hidden, false, "The message bar is now shown"); + is( + messageBar.querySelector("button").getAttribute("locales"), + "he,en-US", + "The locales are set on the message bar button" + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); + +add_task(async function testLiveLanguageReloading() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", true], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,fr,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale starts as English." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order starts as what was initially requested." + ); + + // French and English are both LTR languages. + let french = availableLocales.find(item => item.value == "fr"); + + french.click(); + available.menupopup.hidePopup(); + + is( + Services.locale.appLocaleAsBCP47, + "fr", + "The app locale was changed to French" + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["fr", "en-US", "he", "de"], + "The locale order is switched to french first." + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); + +add_task(async function testLiveLanguageReloadingBidiOff() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", true], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,fr,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale starts as English." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order starts as what was initially requested." + ); + + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // English is LTR and Hebrew is RTL. + let hebrew = availableLocales.find(item => item.value == "he"); + + hebrew.click(); + available.menupopup.hidePopup(); + + await BrowserTestUtils.waitForCondition( + () => !messageBar.hidden, + "Wait for message bar to show" + ); + + is(messageBar.hidden, false, "The message bar is now shown"); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale remains in English" + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order did not change." + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); + +add_task(async function testLiveLanguageReloadingBidiOn() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", true], + ["intl.multilingual.liveReloadBidirectional", true], + ["intl.locale.requested", "en-US,fr,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale starts as English." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order starts as what was initially requested." + ); + + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // English is LTR and Hebrew is RTL. + let hebrew = availableLocales.find(item => item.value == "he"); + + hebrew.click(); + available.menupopup.hidePopup(); + + is(messageBar.hidden, true, "The message bar is still hidden"); + + is( + Services.locale.appLocaleAsBCP47, + "he", + "The app locale was changed to Hebrew." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["he", "en-US", "fr", "de"], + "The locale changed with Hebrew first." + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); diff --git a/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js b/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js new file mode 100644 index 0000000000..bc928656ab --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var originalWindowHeight; +registerCleanupFunction(function () { + window.resizeTo(window.outerWidth, originalWindowHeight); + while (gBrowser.tabs[1]) { + gBrowser.removeTab(gBrowser.tabs[1]); + } +}); + +add_task(async function () { + originalWindowHeight = window.outerHeight; + window.resizeTo(window.outerWidth, 300); + let prefs = await openPreferencesViaOpenPreferencesAPI("paneSearch", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneSearch", "Search pane was selected"); + let mainContent = gBrowser.contentDocument.querySelector(".main-content"); + mainContent.scrollTop = 50; + is(mainContent.scrollTop, 50, "main-content should be scrolled 50 pixels"); + + await gBrowser.contentWindow.gotoPref("paneGeneral"); + + is( + mainContent.scrollTop, + 0, + "Switching to a different category should reset the scroll position" + ); +}); diff --git a/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js b/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js new file mode 100644 index 0000000000..26e79b648d --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test opening to the differerent panes and subcategories in Preferences +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy"); + is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected"); + prefs = await openPreferencesViaHash("privacy"); + is( + prefs.selectedPane, + "panePrivacy", + "Privacy pane is selected when hash is 'privacy'" + ); + prefs = await openPreferencesViaOpenPreferencesAPI("nonexistant-category"); + is( + prefs.selectedPane, + "paneGeneral", + "General pane is selected by default when a nonexistant-category is requested" + ); + prefs = await openPreferencesViaHash("nonexistant-category"); + is( + prefs.selectedPane, + "paneGeneral", + "General pane is selected when hash is a nonexistant-category" + ); + prefs = await openPreferencesViaHash(); + is(prefs.selectedPane, "paneGeneral", "General pane is selected by default"); + prefs = await openPreferencesViaOpenPreferencesAPI("privacy-reports", { + leaveOpen: true, + }); + is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected by default"); + let doc = gBrowser.contentDocument; + is( + doc.location.hash, + "#privacy", + "The subcategory should be removed from the URI" + ); + await TestUtils.waitForCondition( + () => doc.querySelector(".spotlight"), + "Wait for the reports section is spotlighted." + ); + is( + doc.querySelector(".spotlight").getAttribute("data-subcategory"), + "reports", + "The reports section is spotlighted." + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test opening Preferences with subcategory on an existing Preferences tab. See bug 1358475. +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("general", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane is selected by default"); + let doc = gBrowser.contentDocument; + is( + doc.location.hash, + "#general", + "The subcategory should be removed from the URI" + ); + // The reasons that here just call the `openPreferences` API without the helping function are + // - already opened one about:preferences tab up there and + // - the goal is to test on the existing tab and + // - using `openPreferencesViaOpenPreferencesAPI` would introduce more handling of additional about:blank and unneccessary event + await openPreferences("privacy-reports"); + let selectedPane = gBrowser.contentWindow.history.state; + is(selectedPane, "panePrivacy", "Privacy pane should be selected"); + is( + doc.location.hash, + "#privacy", + "The subcategory should be removed from the URI" + ); + await TestUtils.waitForCondition( + () => doc.querySelector(".spotlight"), + "Wait for the reports section is spotlighted." + ); + is( + doc.querySelector(".spotlight").getAttribute("data-subcategory"), + "reports", + "The reports section is spotlighted." + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test opening to a subcategory displays the correct values for preferences +add_task(async function () { + // Skip if crash reporting isn't enabled since the checkbox will be missing. + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", true]], + }); + await openPreferencesViaOpenPreferencesAPI("privacy-reports", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + ok( + doc.querySelector("#automaticallySubmitCrashesBox").checked, + "Checkbox for automatically submitting crashes should be checked when the pref is true and only Reports are requested" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function () { + // Skip if crash reporting isn't enabled since the checkbox will be missing. + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", false]], + }); + await openPreferencesViaOpenPreferencesAPI("privacy-reports", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + ok( + !doc.querySelector("#automaticallySubmitCrashesBox").checked, + "Checkbox for automatically submitting crashes should not be checked when the pref is false only Reports are requested" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +function openPreferencesViaHash(aPane) { + return new Promise(resolve => { + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:preferences" + (aPane ? "#" + aPane : "") + ); + let newTabBrowser = gBrowser.selectedBrowser; + + newTabBrowser.addEventListener( + "Initialized", + function () { + newTabBrowser.contentWindow.addEventListener( + "load", + async function () { + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + await finalPrefPaneLoaded; + gBrowser.removeCurrentTab(); + resolve({ selectedPane }); + }, + { once: true } + ); + }, + { capture: true, once: true } + ); + }); +} diff --git a/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js new file mode 100644 index 0000000000..ef387f9a9d --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js @@ -0,0 +1,116 @@ +const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; + +add_task(async function () { + waitForExplicitFinish(); + + const tabURL = + getRootDirectory(gTestPath) + + "browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml"; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: tabURL }, + async function (browser) { + let doc = browser.contentDocument; + let container = doc.getElementById("container"); + + // Test button + let button = doc.getElementById("button"); + button.focus(); + let initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + await checkPageScrolling(container, "button", initialScrollTop); + + // Test checkbox + let checkbox = doc.getElementById("checkbox"); + checkbox.focus(); + initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + ok(checkbox.checked, "Checkbox is checked"); + await checkPageScrolling(container, "checkbox", initialScrollTop); + + // Test radio + let radiogroup = doc.getElementById("radiogroup"); + radiogroup.focus(); + initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + await checkPageScrolling(container, "radio", initialScrollTop); + } + ); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#search" }, + async function (browser) { + let doc = browser.contentDocument; + let container = doc.getElementsByClassName("main-content")[0]; + + // Test search + let engineList = doc.getElementById("engineList"); + engineList.focus(); + let initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + is( + engineList.view.selection.currentIndex, + 0, + "Search engineList is selected" + ); + EventUtils.sendString(" "); + await checkPageScrolling( + container, + "search engineList", + initialScrollTop + ); + } + ); + + // Test session restore + const CRASH_URL = "about:mozilla"; + const CRASH_FAVICON = "chrome://branding/content/icon32.png"; + const CRASH_SHENTRY = { url: CRASH_URL }; + const CRASH_TAB = { entries: [CRASH_SHENTRY], image: CRASH_FAVICON }; + const CRASH_STATE = { windows: [{ tabs: [CRASH_TAB] }] }; + + const TAB_URL = "about:sessionrestore"; + const TAB_FORMDATA = { url: TAB_URL, id: { sessionData: CRASH_STATE } }; + const TAB_SHENTRY = { url: TAB_URL, triggeringPrincipal_base64 }; + const TAB_STATE = { entries: [TAB_SHENTRY], formdata: TAB_FORMDATA }; + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + + // Fake a post-crash tab + SessionStore.setTabState(tab, JSON.stringify(TAB_STATE)); + + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let doc = tab.linkedBrowser.contentDocument; + + // Make body scrollable + doc.body.style.height = doc.body.clientHeight + 100 + "px"; + + let tabsToggle = doc.getElementById("tabsToggle"); + tabsToggle.focus(); + let initialScrollTop = doc.documentElement.scrollTop; + EventUtils.sendString(" "); + await checkPageScrolling( + doc.documentElement, + "session restore", + initialScrollTop + ); + + gBrowser.removeCurrentTab(); + finish(); +}); + +function checkPageScrolling(container, type, initialScrollTop = 0) { + return new Promise(resolve => { + setTimeout(() => { + is( + container.scrollTop, + initialScrollTop, + "Page should not scroll when " + type + " flipped" + ); + resolve(); + }, 0); + }); +} diff --git a/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml new file mode 100644 index 0000000000..8b3e39a39c --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- + XUL Widget Test for Bug 1184989 + --> +<window title="Bug 1184989 Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<vbox id="container" style="height: 200px; overflow: auto;"> + <vbox style="height: 500px;"> + <hbox> + <button id="button" label="button" /> + </hbox> + + <hbox> + <checkbox id="checkbox" label="checkbox" /> + </hbox> + + <hbox> + <radiogroup id="radiogroup"> + <radio id="radio" label="radio" /> + </radiogroup> + </hbox> + </vbox> +</vbox> + +</window> diff --git a/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js b/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js new file mode 100644 index 0000000000..cd5b00b5f2 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + Services.prefs.lockPref("browser.download.useDownloadDir"); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + var downloadFolder = doc.getElementById("downloadFolder"); + var chooseFolder = doc.getElementById("chooseFolder"); + is( + downloadFolder.disabled, + false, + "Download folder field should not be disabled." + ); + is(chooseFolder.disabled, false, "Choose folder should not be disabled."); + + gBrowser.removeCurrentTab(); + + Services.prefs.unlockPref("browser.download.useDownloadDir"); +}); diff --git a/browser/components/preferences/tests/browser_bug1579418.js b/browser/components/preferences/tests/browser_bug1579418.js new file mode 100644 index 0000000000..a179ae9936 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1579418.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function default_homepage_test() { + let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage"); + let oldStartpagePref = Services.prefs.getIntPref("browser.startup.page"); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + let homeMode = doc.getElementById("homeMode"); + let customSettings = doc.getElementById("customSettings"); + + // HOME_MODE_FIREFOX_HOME + homeMode.value = 0; + + homeMode.dispatchEvent(new Event("command")); + + is(Services.prefs.getCharPref("browser.startup.homepage"), "about:home"); + + // HOME_MODE_BLANK + homeMode.value = 1; + + homeMode.dispatchEvent(new Event("command")); + + await TestUtils.waitForCondition( + () => customSettings.hidden === true, + "Wait for customSettings to be hidden." + ); + + is( + Services.prefs.getCharPref("browser.startup.homepage"), + "chrome://browser/content/blanktab.html" + ); + + // HOME_MODE_CUSTOM + homeMode.value = 2; + + homeMode.dispatchEvent(new Event("command")); + + await TestUtils.waitForCondition( + () => customSettings.hidden === false, + "Wait for customSettings to be shown." + ); + + is(customSettings.hidden, false, "homePageURL should be visible"); + + registerCleanupFunction(async () => { + Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref); + Services.prefs.setIntPref("browser.startup.page", oldStartpagePref); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); diff --git a/browser/components/preferences/tests/browser_bug410900.js b/browser/components/preferences/tests/browser_bug410900.js new file mode 100644 index 0000000000..4fd4bb21f5 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug410900.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + // Setup a phony handler to ensure the app pane will be populated. + var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler.name = "App pane alive test"; + handler.uriTemplate = "http://test.mozilla.org/%s"; + + var extps = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + var info = extps.getProtocolHandlerInfo("apppanetest"); + info.possibleApplicationHandlers.appendElement(handler); + + var hserv = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + hserv.store(info); + + openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }) + .then(() => gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList) + .then(() => runTest(gBrowser.selectedBrowser.contentWindow)); +} + +function runTest(win) { + var rbox = win.document.getElementById("handlersView"); + ok(rbox, "handlersView is present"); + + var items = rbox && rbox.getElementsByTagName("richlistitem"); + ok(items && !!items.length, "App handler list populated"); + + var handlerAdded = false; + for (let i = 0; i < items.length; i++) { + if (items[i].getAttribute("type") == "apppanetest") { + handlerAdded = true; + } + } + ok(handlerAdded, "apppanetest protocol handler was successfully added"); + + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/components/preferences/tests/browser_bug731866.js b/browser/components/preferences/tests/browser_bug731866.js new file mode 100644 index 0000000000..b090535a49 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug731866.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const browserContainersGroupDisabled = !SpecialPowers.getBoolPref( + "privacy.userContext.ui.enabled" +); +const cookieBannerHandlingDisabled = !SpecialPowers.getBoolPref( + "cookiebanners.ui.desktop.enabled" +); +const updatePrefContainers = ["updatesCategory", "updateApp"]; +const updateContainersGroupDisabled = + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId"); + +function test() { + waitForExplicitFinish(); + open_preferences(runTest); +} + +var gElements; + +function checkElements(expectedPane) { + for (let element of gElements) { + // keyset elements fail is_element_visible checks because they are never visible. + // special-case the drmGroup item because its visibility depends on pref + OS version + if (element.nodeName == "keyset" || element.id === "drmGroup") { + continue; + } + + // The browserContainersGroup is still only pref-on on Nightly + if ( + element.id == "browserContainersGroup" && + browserContainersGroupDisabled + ) { + is_element_hidden( + element, + "Disabled browserContainersGroup should be hidden" + ); + continue; + } + + // Cookie Banner Handling is currently disabled by default (bug 1800679) + if ( + element.id == "cookieBannerHandlingGroup" && + cookieBannerHandlingDisabled + ) { + is_element_hidden( + element, + "Disabled cookieBannerHandlingGroup should be hidden" + ); + continue; + } + + // Update prefs are hidden when running an MSIX build + if ( + updatePrefContainers.includes(element.id) && + updateContainersGroupDisabled + ) { + is_element_hidden(element, "Disabled " + element + " should be hidden"); + continue; + } + + let attributeValue = element.getAttribute("data-category"); + let suffix = " (id=" + element.id + ")"; + if (attributeValue == "pane" + expectedPane) { + is_element_visible( + element, + expectedPane + " elements should be visible" + suffix + ); + } else { + is_element_hidden( + element, + "Elements not in " + expectedPane + " should be hidden" + suffix + ); + } + } +} + +async function runTest(win) { + is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded"); + + let tab = win.document; + gElements = tab.getElementById("mainPrefPane").children; + + let panes = ["General", "Search", "Privacy", "Sync"]; + + for (let pane of panes) { + await win.gotoPref("pane" + pane); + checkElements(pane); + } + + gBrowser.removeCurrentTab(); + win.close(); + finish(); +} diff --git a/browser/components/preferences/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/tests/browser_bug795764_cachedisabled.js new file mode 100644 index 0000000000..97f5aaf48f --- /dev/null +++ b/browser/components/preferences/tests/browser_bug795764_cachedisabled.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + // Adding one fake site so that the SiteDataManager would run. + // Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager. + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://www.foo.com" + ); + Services.perms.addFromPrincipal( + principal, + "persistent-storage", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + registerCleanupFunction(function () { + Services.perms.removeFromPrincipal(principal, "persistent-storage"); + }); + + SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.ui.enabled", true]], + }).then(() => open_preferences(runTest)); +} + +async function runTest(win) { + is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded"); + + let tab = win.document; + let elements = tab.getElementById("mainPrefPane").children; + + // Test if privacy pane is opened correctly + await win.gotoPref("panePrivacy"); + for (let element of elements) { + let attributeValue = element.getAttribute("data-category"); + + // Ignore the cookie banner handling section, as it is currently preffed + // off by default (bug 1800679). + if (element.id === "cookieBannerHandlingGroup") { + continue; + } + + if (attributeValue == "panePrivacy") { + is_element_visible(element, "HTTPSOnly should be visible"); + + is_element_visible( + element, + `Privacy element of id=${element.id} should be visible` + ); + } else { + is_element_hidden( + element, + `Non-Privacy element of id=${element.id} should be hidden` + ); + } + } + + gBrowser.removeCurrentTab(); + win.close(); + finish(); +} diff --git a/browser/components/preferences/tests/browser_cert_export.js b/browser/components/preferences/tests/browser_cert_export.js new file mode 100644 index 0000000000..48769f84e6 --- /dev/null +++ b/browser/components/preferences/tests/browser_cert_export.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var MockFilePicker = SpecialPowers.MockFilePicker; + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + info("create testsavedir!"); + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + info("return from createTempSaveDir: " + saveDir.path); + return saveDir; +} + +// Create the folder the certificates will be saved into. +var destDir = createTemporarySaveDirectory(); +registerCleanupFunction(function () { + destDir.remove(true); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +function stringOrArrayEquals(actual, expected, message) { + is( + typeof actual, + typeof expected, + "actual, expected should have the same type" + ); + if (typeof expected == "string") { + is(actual, expected, message); + } else { + is(actual.toString(), expected.toString(), message); + } +} + +var dialogWin; +var exportButton; +var expectedCert; + +async function setupTest() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let certButton = gBrowser.selectedBrowser.contentDocument.getElementById( + "viewCertificatesButton" + ); + certButton.scrollIntoView(); + let certDialogLoaded = promiseLoadSubDialog( + "chrome://pippki/content/certManager.xhtml" + ); + certButton.click(); + dialogWin = await certDialogLoaded; + let doc = dialogWin.document; + doc.getElementById("certmanagertabs").selectedTab = + doc.getElementById("ca_tab"); + let treeView = doc.getElementById("ca-tree").view; + // Select any which cert. Ignore parent rows (ie rows without certs): + for (let i = 0; i < treeView.rowCount; i++) { + treeView.selection.select(i); + dialogWin.getSelectedCerts(); + let certs = dialogWin.selected_certs; // yuck... but this is how the dialog works. + if (certs && certs.length == 1 && certs[0]) { + expectedCert = certs[0]; + // OK, we managed to select a cert! + break; + } + } + + exportButton = doc.getElementById("ca_exportButton"); + is(exportButton.disabled, false, "Should enable export button"); +} + +async function checkCertExportWorks( + exportType, + encoding, + expectedFileContents +) { + MockFilePicker.displayDirectory = destDir; + var destFile = destDir.clone(); + MockFilePicker.init(window); + MockFilePicker.filterIndex = exportType; + MockFilePicker.showCallback = function (fp) { + info("showCallback"); + let fileName = fp.defaultString; + info("fileName: " + fileName); + destFile.append(fileName); + MockFilePicker.setFiles([destFile]); + info("done showCallback"); + }; + let finishedExporting = TestUtils.topicObserved("cert-export-finished"); + exportButton.click(); + await finishedExporting; + MockFilePicker.cleanup(); + if (destFile && destFile.exists()) { + let contents; + if (encoding === "utf-8") { + contents = await IOUtils.readUTF8(destFile.path); + } else { + is(encoding, "", "expected either utf-8 or empty string for encoding"); + contents = await IOUtils.read(destFile.path); + } + stringOrArrayEquals( + contents, + expectedFileContents, + "Should have written correct contents" + ); + destFile.remove(false); + } else { + ok(false, "No cert saved!"); + } +} + +add_task(setupTest); + +add_task(async function checkCertPEMExportWorks() { + let expectedContents = dialogWin.getPEMString(expectedCert); + await checkCertExportWorks(0, /* 0 = PEM */ "utf-8", expectedContents); +}); + +add_task(async function checkCertPEMChainExportWorks() { + let expectedContents = dialogWin.getPEMString(expectedCert); + await checkCertExportWorks( + 1, // 1 = PEM chain, but the chain is of length 1 + "utf-8", + expectedContents + ); +}); + +add_task(async function checkCertDERExportWorks() { + let expectedContents = Uint8Array.from(expectedCert.getRawDER()); + await checkCertExportWorks(2, /* 2 = DER */ "", expectedContents); +}); + +function stringToTypedArray(str) { + let arr = new Uint8Array(str.length); + for (let i = 0; i < arr.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; +} + +add_task(async function checkCertPKCS7ExportWorks() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + let expectedContents = stringToTypedArray(certdb.asPKCS7Blob([expectedCert])); + await checkCertExportWorks(3, /* 3 = PKCS7 */ "", expectedContents); +}); + +add_task(async function checkCertPKCS7ChainExportWorks() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + let expectedContents = stringToTypedArray(certdb.asPKCS7Blob([expectedCert])); + await checkCertExportWorks( + 4, // 4 = PKCS7 chain, but the chain is of length 1 + "", + expectedContents + ); +}); diff --git a/browser/components/preferences/tests/browser_change_app_handler.js b/browser/components/preferences/tests/browser_change_app_handler.js new file mode 100644 index 0000000000..b4ca92592f --- /dev/null +++ b/browser/components/preferences/tests/browser_change_app_handler.js @@ -0,0 +1,155 @@ +var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); +var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +SimpleTest.requestCompleteLog(); + +function setupFakeHandler() { + let info = gMimeSvc.getFromTypeAndExtension("text/plain", "foo.txt"); + ok( + info.possibleLocalHandlers.length, + "Should have at least one known handler" + ); + let handler = info.possibleLocalHandlers.queryElementAt( + 0, + Ci.nsILocalHandlerApp + ); + + let infoToModify = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + infoToModify.possibleApplicationHandlers.appendElement(handler); + + gHandlerSvc.store(infoToModify); +} + +add_task(async function () { + setupFakeHandler(); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + let ourItem = container.querySelector( + "richlistitem[type='text/x-test-handler']" + ); + ok(ourItem, "handlersView is present"); + ourItem.scrollIntoView(); + container.selectItem(ourItem); + ok(ourItem.selected, "Should be able to select our item."); + + let list = ourItem.querySelector(".actionsMenu"); + + let chooseItem = list.menupopup.querySelector(".choose-app-item"); + let dialogLoadedPromise = promiseLoadSubDialog( + "chrome://global/content/appPicker.xhtml" + ); + let cmdEvent = win.document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + win, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + chooseItem.dispatchEvent(cmdEvent); + + let dialog = await dialogLoadedPromise; + info("Dialog loaded"); + + let dialogDoc = dialog.document; + let dialogElement = dialogDoc.getElementById("app-picker"); + let dialogList = dialogDoc.getElementById("app-picker-listbox"); + dialogList.selectItem(dialogList.firstElementChild); + let selectedApp = dialogList.firstElementChild.handlerApp; + dialogElement.acceptDialog(); + + // Verify results are correct in mime service: + let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null); + ok( + mimeInfo.preferredApplicationHandler.equals(selectedApp), + "App should be set as preferred." + ); + + // Check that we display this result: + ok(list.selectedItem, "Should have a selected item"); + ok( + mimeInfo.preferredApplicationHandler.equals(list.selectedItem.handlerApp), + "App should be visible as preferred item." + ); + + // Now try to 'manage' this list: + dialogLoadedPromise = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/applicationManager.xhtml" + ); + + let manageItem = list.menupopup.querySelector(".manage-app-item"); + cmdEvent = win.document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + win, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + manageItem.dispatchEvent(cmdEvent); + + dialog = await dialogLoadedPromise; + info("Dialog loaded the second time"); + + dialogDoc = dialog.document; + dialogElement = dialogDoc.getElementById("appManager"); + dialogList = dialogDoc.getElementById("appList"); + let itemToRemove = dialogList.querySelector( + 'richlistitem > label[value="' + selectedApp.name + '"]' + ).parentNode; + dialogList.selectItem(itemToRemove); + let itemsBefore = dialogList.children.length; + dialogDoc.getElementById("remove").click(); + ok(!itemToRemove.parentNode, "Item got removed from DOM"); + is(dialogList.children.length, itemsBefore - 1, "Item got removed"); + dialogElement.acceptDialog(); + + // Verify results are correct in mime service: + mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null); + ok( + !mimeInfo.preferredApplicationHandler, + "App should no longer be set as preferred." + ); + + // Check that we display this result: + ok(list.selectedItem, "Should have a selected item"); + ok( + !list.selectedItem.handlerApp, + "No app should be visible as preferred item." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +registerCleanupFunction(function () { + let infoToModify = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + gHandlerSvc.remove(infoToModify); +}); diff --git a/browser/components/preferences/tests/browser_checkspelling.js b/browser/components/preferences/tests/browser_checkspelling.js new file mode 100644 index 0000000000..a7895b4201 --- /dev/null +++ b/browser/components/preferences/tests/browser_checkspelling.js @@ -0,0 +1,34 @@ +add_task(async function () { + SpecialPowers.pushPrefEnv({ set: [["layout.spellcheckDefault", 2]] }); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let checkbox = doc.querySelector("#checkSpelling"); + is( + checkbox.checked, + Services.prefs.getIntPref("layout.spellcheckDefault") == 2, + "checkbox should represent pref value before clicking on checkbox" + ); + ok( + checkbox.checked, + "checkbox should be checked before clicking on checkbox" + ); + + checkbox.click(); + + is( + checkbox.checked, + Services.prefs.getIntPref("layout.spellcheckDefault") == 2, + "checkbox should represent pref value after clicking on checkbox" + ); + ok( + !checkbox.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_connection.js b/browser/components/preferences/tests/browser_connection.js new file mode 100644 index 0000000000..01cdf571f2 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection.js @@ -0,0 +1,145 @@ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + // network.proxy.type needs to be backed up and restored because mochitest + // changes this setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function () { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.no_proxies_on"); + // On accepting the dialog, we also write TRR values, so we need to clear + // them. They are tested separately in browser_privacy_dnsoverhttps.js. + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + }); + + let connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + + /* + The connection dialog alone won't save onaccept since it uses type="child", + so it has to be opened as a sub dialog of the main pref tab. + Open the main tab here. + */ + open_preferences(async function tabOpened(aContentWindow) { + is( + gBrowser.currentURI.spec, + "about:preferences", + "about:preferences loaded" + ); + let dialog = await openAndLoadSubDialog(connectionURL); + let dialogElement = dialog.document.getElementById("ConnectionsDialog"); + let dialogClosingPromise = BrowserTestUtils.waitForEvent( + dialogElement, + "dialogclosing" + ); + + ok(dialog, "connection window opened"); + runConnectionTests(dialog); + dialogElement.acceptDialog(); + + let dialogClosingEvent = await dialogClosingPromise; + ok(dialogClosingEvent, "connection window closed"); + // runConnectionTests will have changed this pref - make sure it was + // sanitized correctly when the dialog was accepted + is( + Services.prefs.getCharPref("network.proxy.no_proxies_on"), + ".a.com,.b.com,.c.com", + "no_proxies_on pref has correct value" + ); + gBrowser.removeCurrentTab(); + finish(); + }); +} + +// run a bunch of tests on the window containing connection.xul +function runConnectionTests(win) { + let doc = win.document; + let networkProxyNone = doc.getElementById("networkProxyNone"); + let networkProxyNonePref = win.Preferences.get("network.proxy.no_proxies_on"); + let networkProxyTypePref = win.Preferences.get("network.proxy.type"); + + // make sure the networkProxyNone textbox is formatted properly + is(networkProxyNone.localName, "textarea", "networkProxyNone is a textarea"); + is( + networkProxyNone.getAttribute("rows"), + "2", + "networkProxyNone textbox has two rows" + ); + + // make sure manual proxy controls are disabled when the window is opened + let networkProxyHTTP = doc.getElementById("networkProxyHTTP"); + is(networkProxyHTTP.disabled, true, "networkProxyHTTP textbox is disabled"); + + // check if sanitizing the given input for the no_proxies_on pref results in + // expected string + function testSanitize(input, expected, errorMessage) { + networkProxyNonePref.value = input; + win.gConnectionsDialog.sanitizeNoProxiesPref(); + is(networkProxyNonePref.value, expected, errorMessage); + } + + // change this pref so proxy exceptions are actually configurable + networkProxyTypePref.value = 1; + is(networkProxyNone.disabled, false, "networkProxyNone textbox is enabled"); + + testSanitize(".a.com", ".a.com", "sanitize doesn't mess up single filter"); + testSanitize( + ".a.com, .b.com, .c.com", + ".a.com, .b.com, .c.com", + "sanitize doesn't mess up multiple comma/space sep filters" + ); + testSanitize( + ".a.com\n.b.com", + ".a.com,.b.com", + "sanitize turns line break into comma" + ); + testSanitize( + ".a.com,\n.b.com", + ".a.com,.b.com", + "sanitize doesn't add duplicate comma after comma" + ); + testSanitize( + ".a.com\n,.b.com", + ".a.com,.b.com", + "sanitize doesn't add duplicate comma before comma" + ); + testSanitize( + ".a.com,\n,.b.com", + ".a.com,,.b.com", + "sanitize doesn't add duplicate comma surrounded by commas" + ); + testSanitize( + ".a.com, \n.b.com", + ".a.com, .b.com", + "sanitize doesn't add comma after comma/space" + ); + testSanitize( + ".a.com\n .b.com", + ".a.com, .b.com", + "sanitize adds comma before space" + ); + testSanitize( + ".a.com\n\n\n;;\n;\n.b.com", + ".a.com,.b.com", + "sanitize only adds one comma per substring of bad chars" + ); + testSanitize( + ".a.com,,.b.com", + ".a.com,,.b.com", + "duplicate commas from user are untouched" + ); + testSanitize( + ".a.com\n.b.com\n.c.com,\n.d.com,\n.e.com", + ".a.com,.b.com,.c.com,.d.com,.e.com", + "sanitize replaces things globally" + ); + + // will check that this was sanitized properly after window closes + networkProxyNonePref.value = ".a.com;.b.com\n.c.com"; +} diff --git a/browser/components/preferences/tests/browser_connection_bug1445991.js b/browser/components/preferences/tests/browser_connection_bug1445991.js new file mode 100644 index 0000000000..ecb5068a26 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_bug1445991.js @@ -0,0 +1,31 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the disabled status of the autoconfig Reload button when the proxy type +// is autoconfig (network.proxy.type == 2). +add_task(async function testAutoconfigReloadButton() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.proxy.type", 2], + ["network.proxy.autoconfig_url", "file:///nonexistent.pac"], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + const connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + const promiseDialogLoaded = promiseLoadSubDialog(connectionURL); + gBrowser.contentDocument.getElementById("connectionSettings").click(); + const dialog = await promiseDialogLoaded; + + ok( + !dialog.document.getElementById("autoReload").disabled, + "Reload button is enabled when proxy type is autoconfig" + ); + + dialog.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_connection_bug1505330.js b/browser/components/preferences/tests/browser_connection_bug1505330.js new file mode 100644 index 0000000000..94dfd4706e --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_bug1505330.js @@ -0,0 +1,31 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the disabled status of the autoconfig Reload button when the proxy type +// is autoconfig (network.proxy.type == 2). +add_task(async function testAutoconfigReloadButton() { + Services.prefs.lockPref("signon.autologin.proxy"); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + const connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + const promiseDialogLoaded = promiseLoadSubDialog(connectionURL); + gBrowser.contentDocument.getElementById("connectionSettings").click(); + const dialog = await promiseDialogLoaded; + + ok( + !dialog.document.getElementById("networkProxyType").firstChild.disabled, + "Connection options should not be disabled" + ); + ok( + dialog.document.getElementById("autologinProxy").disabled, + "Proxy autologin should be disabled" + ); + + dialog.close(); + Services.prefs.unlockPref("signon.autologin.proxy"); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_connection_bug388287.js b/browser/components/preferences/tests/browser_connection_bug388287.js new file mode 100644 index 0000000000..d6f0c3c9d0 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_bug388287.js @@ -0,0 +1,124 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + const connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + let closeable = false; + let finalTest = false; + + // The changed preferences need to be backed up and restored because this mochitest + // changes them setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function () { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + for (let proxyType of ["http", "ssl", "socks"]) { + Services.prefs.clearUserPref("network.proxy." + proxyType); + Services.prefs.clearUserPref("network.proxy." + proxyType + "_port"); + if (proxyType == "http") { + continue; + } + Services.prefs.clearUserPref("network.proxy.backup." + proxyType); + Services.prefs.clearUserPref( + "network.proxy.backup." + proxyType + "_port" + ); + } + // On accepting the dialog, we also write TRR values, so we need to clear + // them. They are tested separately in browser_privacy_dnsoverhttps.js. + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + }); + + /* + The connection dialog alone won't save onaccept since it uses type="child", + so it has to be opened as a sub dialog of the main pref tab. + Open the main tab here. + */ + open_preferences(async function tabOpened(aContentWindow) { + let dialog, dialogClosingPromise, dialogElement; + let proxyTypePref, sharePref, httpPref, httpPortPref; + + // Convenient function to reset the variables for the new window + async function setDoc() { + if (closeable) { + let dialogClosingEvent = await dialogClosingPromise; + ok(dialogClosingEvent, "Connection dialog closed"); + } + + if (finalTest) { + gBrowser.removeCurrentTab(); + finish(); + return; + } + + dialog = await openAndLoadSubDialog(connectionURL); + dialogElement = dialog.document.getElementById("ConnectionsDialog"); + dialogClosingPromise = BrowserTestUtils.waitForEvent( + dialogElement, + "dialogclosing" + ); + + proxyTypePref = dialog.Preferences.get("network.proxy.type"); + sharePref = dialog.Preferences.get("network.proxy.share_proxy_settings"); + httpPref = dialog.Preferences.get("network.proxy.http"); + httpPortPref = dialog.Preferences.get("network.proxy.http_port"); + } + + // This batch of tests should not close the dialog + await setDoc(); + + // Testing HTTP port 0 with share on + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = "localhost"; + httpPortPref.value = 0; + dialogElement.acceptDialog(); + + // Testing HTTP port 0 + FTP port 80 with share off + sharePref.value = false; + dialogElement.acceptDialog(); + + // Testing HTTP port 80 + FTP port 0 with share off + httpPortPref.value = 80; + dialogElement.acceptDialog(); + + // From now on, the dialog should close since we are giving it legitimate inputs. + // The test will timeout if the onbeforeaccept kicks in erroneously. + closeable = true; + + // Both ports 80, share on + httpPortPref.value = 80; + dialogElement.acceptDialog(); + + // HTTP 80, FTP 0, with share on + await setDoc(); + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = "localhost"; + httpPortPref.value = 80; + dialogElement.acceptDialog(); + + // HTTP host empty, port 0 with share on + await setDoc(); + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = ""; + httpPortPref.value = 0; + dialogElement.acceptDialog(); + + // HTTP 0, but in no proxy mode + await setDoc(); + proxyTypePref.value = 0; + sharePref.value = true; + httpPref.value = "localhost"; + httpPortPref.value = 0; + + // This is the final test, don't spawn another connection window + finalTest = true; + dialogElement.acceptDialog(); + await setDoc(); + }); +} diff --git a/browser/components/preferences/tests/browser_containers_name_input.js b/browser/components/preferences/tests/browser_containers_name_input.js new file mode 100644 index 0000000000..38785d3cb0 --- /dev/null +++ b/browser/components/preferences/tests/browser_containers_name_input.js @@ -0,0 +1,72 @@ +const CONTAINERS_URL = + "chrome://browser/content/preferences/dialogs/containers.xhtml"; + +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("containers", { leaveOpen: true }); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +add_task(async function () { + async function openDialog() { + let doc = gBrowser.selectedBrowser.contentDocument; + + let dialogPromise = promiseLoadSubDialog(CONTAINERS_URL); + + let addButton = doc.getElementById("containersAdd"); + addButton.doCommand(); + + let dialog = await dialogPromise; + + return dialog.document; + } + + let { contentDocument } = gBrowser.selectedBrowser; + let containerNodes = Array.from( + contentDocument.querySelectorAll("[data-category=paneContainers]") + ); + ok( + containerNodes.find(node => node.getBoundingClientRect().width > 0), + "Should actually be showing the container nodes." + ); + + let doc = await openDialog(); + + let name = doc.getElementById("name"); + let btnApplyChanges = doc.querySelector("dialog").getButton("accept"); + + Assert.equal(name.value, "", "The name textbox should initlally be empty"); + Assert.ok( + btnApplyChanges.disabled, + "The done button should initially be disabled" + ); + + function setName(value) { + name.value = value; + + let event = new doc.defaultView.InputEvent("input", { data: value }); + SpecialPowers.dispatchEvent(doc.defaultView, name, event); + } + + setName("test"); + + Assert.ok( + !btnApplyChanges.disabled, + "The done button should be enabled when the value is not empty" + ); + + setName(""); + + Assert.ok( + btnApplyChanges.disabled, + "The done button should be disabled when the value is empty" + ); + + setName("\u0009\u000B\u000C\u0020\u00A0\uFEFF\u000A\u000D\u2028\u2029"); + + Assert.ok( + btnApplyChanges.disabled, + "The done button should be disabled when the value contains only whitespaces" + ); +}); diff --git a/browser/components/preferences/tests/browser_contentblocking.js b/browser/components/preferences/tests/browser_contentblocking.js new file mode 100644 index 0000000000..19bd45153b --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking.js @@ -0,0 +1,1382 @@ +/* eslint-env webextensions */ + +"use strict"; + +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const TP_PREF = "privacy.trackingprotection.enabled"; +const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled"; +const NCB_PREF = "network.cookie.cookieBehavior"; +const NCBP_PREF = "network.cookie.cookieBehavior.pbmode"; +const CAT_PREF = "browser.contentblocking.category"; +const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled"; +const STP_PREF = "privacy.trackingprotection.socialtracking.enabled"; +const CM_PREF = "privacy.trackingprotection.cryptomining.enabled"; +const EMAIL_TP_PREF = "privacy.trackingprotection.emailtracking.enabled"; +const EMAIL_TP_PBM_PREF = + "privacy.trackingprotection.emailtracking.pbmode.enabled"; +const LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled"; +const REFERRER_PREF = "network.http.referer.disallowCrossSiteRelaxingDefault"; +const REFERRER_TOP_PREF = + "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation"; +const OCSP_PREF = "privacy.partition.network_state.ocsp_cache"; +const QUERY_PARAM_STRIP_PREF = "privacy.query_stripping.enabled"; +const QUERY_PARAM_STRIP_PBM_PREF = "privacy.query_stripping.enabled.pbmode"; +const PREF_TEST_NOTIFICATIONS = + "browser.safebrowsing.test-notifications.enabled"; +const STRICT_PREF = "browser.contentblocking.features.strict"; +const PRIVACY_PAGE = "about:preferences#privacy"; +const ISOLATE_UI_PREF = + "browser.contentblocking.reject-and-isolate-cookies.preferences.ui.enabled"; +const FPI_PREF = "privacy.firstparty.isolate"; + +const { EnterprisePolicyTesting, PoliciesPrefTracker } = + ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); + +requestLongerTimeout(2); + +add_task(async function testListUpdate() { + SpecialPowers.pushPrefEnv({ set: [[PREF_TEST_NOTIFICATIONS, true]] }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let fingerprintersCheckbox = doc.getElementById( + "contentBlockingFingerprintersCheckbox" + ); + let updateObserved = TestUtils.topicObserved("safebrowsing-update-attempt"); + fingerprintersCheckbox.click(); + let url = (await updateObserved)[1]; + + ok(true, "Has tried to update after the fingerprinting checkbox was toggled"); + is( + url, + "http://127.0.0.1:8888/safebrowsing-dummy/update", + "Using the correct list url to update" + ); + + let cryptominersCheckbox = doc.getElementById( + "contentBlockingCryptominersCheckbox" + ); + updateObserved = TestUtils.topicObserved("safebrowsing-update-attempt"); + cryptominersCheckbox.click(); + url = (await updateObserved)[1]; + + ok(true, "Has tried to update after the cryptomining checkbox was toggled"); + is( + url, + "http://127.0.0.1:8888/safebrowsing-dummy/update", + "Using the correct list url to update" + ); + + gBrowser.removeCurrentTab(); +}); + +// Tests that the content blocking main category checkboxes have the correct default state. +add_task(async function testContentBlockingMainCategory() { + let prefs = [ + [TP_PREF, false], + [TP_PBM_PREF, true], + [STP_PREF, false], + [NCB_PREF, Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER], + [ + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ISOLATE_UI_PREF, true], + [FPI_PREF, false], + ]; + + for (let pref of prefs) { + switch (typeof pref[1]) { + case "boolean": + SpecialPowers.setBoolPref(pref[0], pref[1]); + break; + case "number": + SpecialPowers.setIntPref(pref[0], pref[1]); + break; + } + } + + let checkboxes = [ + "#contentBlockingTrackingProtectionCheckbox", + "#contentBlockingBlockCookiesCheckbox", + ]; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + for (let selector of checkboxes) { + let element = doc.querySelector(selector); + ok(element, "checkbox " + selector + " exists"); + is( + element.getAttribute("checked"), + "true", + "checkbox " + selector + " is checked" + ); + } + + // Ensure the dependent controls of the tracking protection subsection behave properly. + let tpCheckbox = doc.querySelector(checkboxes[0]); + + let dependentControls = ["#trackingProtectionMenu"]; + let alwaysEnabledControls = [ + "#trackingProtectionMenuDesc", + ".content-blocking-category-name", + "#changeBlockListLink", + ]; + + tpCheckbox.checked = true; + + // Select "Always" under "All Detected Trackers". + let menu = doc.querySelector("#trackingProtectionMenu"); + let always = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=always]" + ); + let privateElement = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=private]" + ); + menu.selectedItem = always; + ok( + !privateElement.selected, + "The Only in private windows item should not be selected" + ); + ok(always.selected, "The Always item should be selected"); + + // The first time, privacy-pane-tp-ui-updated won't be dispatched since the + // assignment above is a no-op. + + // Ensure the dependent controls are enabled + checkControlState(doc, dependentControls, true); + checkControlState(doc, alwaysEnabledControls, true); + + let promise = TestUtils.topicObserved("privacy-pane-tp-ui-updated"); + tpCheckbox.click(); + + await promise; + ok(!tpCheckbox.checked, "The checkbox should now be unchecked"); + + // Ensure the dependent controls are disabled + checkControlState(doc, dependentControls, false); + checkControlState(doc, alwaysEnabledControls, true); + + // Make sure the selection in the tracking protection submenu persists after + // a few times of checking and unchecking All Detected Trackers. + // Doing this in a loop in order to avoid typing in the unrolled version manually. + // We need to go from the checked state of the checkbox to unchecked back to + // checked again... + for (let i = 0; i < 3; ++i) { + promise = TestUtils.topicObserved("privacy-pane-tp-ui-updated"); + tpCheckbox.click(); + + await promise; + is(tpCheckbox.checked, i % 2 == 0, "The checkbox should now be unchecked"); + is( + privateElement.selected, + i % 2 == 0, + "The Only in private windows item should be selected by default, when the checkbox is checked" + ); + ok(!always.selected, "The Always item should no longer be selected"); + } + + let cookieMenu = doc.querySelector("#blockCookiesMenu"); + let cookieMenuTrackers = cookieMenu.querySelector( + "menupopup > menuitem[value=trackers]" + ); + let cookieMenuTrackersPlusIsolate = cookieMenu.querySelector( + "menupopup > menuitem[value=trackers-plus-isolate]" + ); + let cookieMenuUnvisited = cookieMenu.querySelector( + "menupopup > menuitem[value=unvisited]" + ); + let cookieMenuAllThirdParties = doc.querySelector( + "menupopup > menuitem[value=all-third-parties]" + ); + let cookieMenuAll = cookieMenu.querySelector( + "menupopup > menuitem[value=always]" + ); + // Select block trackers + cookieMenuTrackers.click(); + ok(cookieMenuTrackers.selected, "The trackers item should be selected"); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block trackers and isolate + cookieMenuTrackersPlusIsolate.click(); + ok( + cookieMenuTrackersPlusIsolate.selected, + "The trackers plus isolate item should be selected" + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block unvisited + cookieMenuUnvisited.click(); + ok(cookieMenuUnvisited.selected, "The unvisited item should be selected"); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block all third party + cookieMenuAllThirdParties.click(); + ok( + cookieMenuAllThirdParties.selected, + "The all-third-parties item should be selected" + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block all third party + cookieMenuAll.click(); + ok(cookieMenuAll.selected, "The all cookies item should be selected"); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + + gBrowser.removeCurrentTab(); + + // Ensure the block-trackers-plus-isolate option only shows in the dropdown if the UI pref is set. + Services.prefs.setBoolPref(ISOLATE_UI_PREF, false); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + doc = gBrowser.contentDocument; + cookieMenuTrackersPlusIsolate = doc.querySelector( + "#blockCookiesMenu menupopup > menuitem[value=trackers-plus-isolate]" + ); + ok( + cookieMenuTrackersPlusIsolate.hidden, + "Trackers plus isolate option is hidden from the dropdown if the ui pref is not set." + ); + + gBrowser.removeCurrentTab(); + + // Ensure the block-trackers-plus-isolate option only shows in the dropdown if FPI is disabled. + SpecialPowers.setIntPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ); + SpecialPowers.setBoolPref(FPI_PREF, true); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + doc = gBrowser.contentDocument; + cookieMenuTrackers = doc.querySelector( + "#blockCookiesMenu menupopup > menuitem[value=trackers]" + ); + cookieMenuTrackersPlusIsolate = doc.querySelector( + "#blockCookiesMenu menupopup > menuitem[value=trackers-plus-isolate]" + ); + ok(cookieMenuTrackers.selected, "The trackers item should be selected"); + ok( + cookieMenuTrackersPlusIsolate.hidden, + "Trackers plus isolate option is hidden from the dropdown if the FPI pref is set." + ); + gBrowser.removeCurrentTab(); + + for (let pref of prefs) { + SpecialPowers.clearUserPref(pref[0]); + } +}); + +// Tests that the content blocking "Standard" category radio sets the prefs to their default values. +add_task(async function testContentBlockingStandardCategory() { + let prefs = { + [TP_PREF]: null, + [TP_PBM_PREF]: null, + [NCB_PREF]: null, + [NCBP_PREF]: null, + [FP_PREF]: null, + [STP_PREF]: null, + [CM_PREF]: null, + [EMAIL_TP_PREF]: null, + [EMAIL_TP_PBM_PREF]: null, + [LEVEL2_PREF]: null, + [REFERRER_PREF]: null, + [REFERRER_TOP_PREF]: null, + [OCSP_PREF]: null, + [QUERY_PARAM_STRIP_PREF]: null, + [QUERY_PARAM_STRIP_PBM_PREF]: null, + }; + + for (let pref in prefs) { + Services.prefs.clearUserPref(pref); + switch (Services.prefs.getPrefType(pref)) { + case Services.prefs.PREF_BOOL: + prefs[pref] = Services.prefs.getBoolPref(pref); + break; + case Services.prefs.PREF_INT: + prefs[pref] = Services.prefs.getIntPref(pref); + break; + case Services.prefs.PREF_STRING: + prefs[pref] = Services.prefs.getCharPref(pref); + break; + default: + ok(false, `Unknown pref type for ${pref}`); + } + } + + Services.prefs.setBoolPref(TP_PREF, true); + Services.prefs.setBoolPref(TP_PBM_PREF, false); + Services.prefs.setIntPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + ); + Services.prefs.setIntPref( + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + ); + Services.prefs.setBoolPref(STP_PREF, !Services.prefs.getBoolPref(STP_PREF)); + Services.prefs.setBoolPref(FP_PREF, !Services.prefs.getBoolPref(FP_PREF)); + Services.prefs.setBoolPref(CM_PREF, !Services.prefs.getBoolPref(CM_PREF)); + Services.prefs.setBoolPref( + EMAIL_TP_PREF, + !Services.prefs.getBoolPref(EMAIL_TP_PREF) + ); + Services.prefs.setBoolPref( + EMAIL_TP_PBM_PREF, + !Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF) + ); + Services.prefs.setBoolPref( + LEVEL2_PREF, + !Services.prefs.getBoolPref(LEVEL2_PREF) + ); + Services.prefs.setBoolPref( + REFERRER_PREF, + !Services.prefs.getBoolPref(REFERRER_PREF) + ); + Services.prefs.setBoolPref( + REFERRER_TOP_PREF, + !Services.prefs.getBoolPref(REFERRER_TOP_PREF) + ); + Services.prefs.setBoolPref(OCSP_PREF, !Services.prefs.getBoolPref(OCSP_PREF)); + Services.prefs.setBoolPref( + QUERY_PARAM_STRIP_PREF, + !Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF) + ); + Services.prefs.setBoolPref( + QUERY_PARAM_STRIP_PBM_PREF, + !Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF) + ); + + for (let pref in prefs) { + switch (Services.prefs.getPrefType(pref)) { + case Services.prefs.PREF_BOOL: + // Account for prefs that may have retained their default value + if (Services.prefs.getBoolPref(pref) != prefs[pref]) { + ok( + Services.prefs.prefHasUserValue(pref), + `modified the pref ${pref}` + ); + } + break; + case Services.prefs.PREF_INT: + if (Services.prefs.getIntPref(pref) != prefs[pref]) { + ok( + Services.prefs.prefHasUserValue(pref), + `modified the pref ${pref}` + ); + } + break; + case Services.prefs.PREF_STRING: + if (Services.prefs.getCharPref(pref) != prefs[pref]) { + ok( + Services.prefs.prefHasUserValue(pref), + `modified the pref ${pref}` + ); + } + break; + default: + ok(false, `Unknown pref type for ${pref}`); + } + } + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let standardRadioOption = doc.getElementById("standardRadio"); + standardRadioOption.click(); + + // TP prefs are reset async to check for extensions controlling them. + await TestUtils.waitForCondition( + () => !Services.prefs.prefHasUserValue(TP_PREF) + ); + + for (let pref in prefs) { + ok(!Services.prefs.prefHasUserValue(pref), `reset the pref ${pref}`); + } + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} has been set to standard` + ); + + gBrowser.removeCurrentTab(); +}); + +// Tests that the content blocking "Strict" category radio sets the prefs to the expected values. +add_task(async function testContentBlockingStrictCategory() { + Services.prefs.setBoolPref(TP_PREF, false); + Services.prefs.setBoolPref(TP_PBM_PREF, false); + Services.prefs.setBoolPref(EMAIL_TP_PREF, false); + Services.prefs.setBoolPref(EMAIL_TP_PBM_PREF, false); + Services.prefs.setBoolPref(LEVEL2_PREF, false); + Services.prefs.setBoolPref(REFERRER_PREF, false); + Services.prefs.setBoolPref(REFERRER_TOP_PREF, false); + Services.prefs.setBoolPref(OCSP_PREF, false); + Services.prefs.setBoolPref(QUERY_PARAM_STRIP_PREF, false); + Services.prefs.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, false); + Services.prefs.setIntPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN + ); + Services.prefs.setIntPref( + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN + ); + let strict_pref = Services.prefs.getStringPref(STRICT_PREF).split(","); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let strictRadioOption = doc.getElementById("strictRadio"); + strictRadioOption.click(); + + // TP prefs are reset async to check for extensions controlling them. + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + // Depending on the definition of the STRICT_PREF, the dependant prefs may have been + // set to varying values. Ensure they have been set according to this definition. + for (let pref of strict_pref) { + switch (pref) { + case "tp": + is( + Services.prefs.getBoolPref(TP_PREF), + true, + `${TP_PREF} has been set to true` + ); + break; + case "-tp": + is( + Services.prefs.getBoolPref(TP_PREF), + false, + `${TP_PREF} has been set to false` + ); + break; + case "tpPrivate": + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + true, + `${TP_PBM_PREF} has been set to true` + ); + break; + case "-tpPrivate": + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + false, + `${TP_PBM_PREF} has been set to false` + ); + break; + case "fp": + is( + Services.prefs.getBoolPref(FP_PREF), + true, + `${FP_PREF} has been set to true` + ); + break; + case "-fp": + is( + Services.prefs.getBoolPref(FP_PREF), + false, + `${FP_PREF} has been set to false` + ); + break; + case "stp": + is( + Services.prefs.getBoolPref(STP_PREF), + true, + `${STP_PREF} has been set to true` + ); + break; + case "-stp": + is( + Services.prefs.getBoolPref(STP_PREF), + false, + `${STP_PREF} has been set to false` + ); + break; + case "cm": + is( + Services.prefs.getBoolPref(CM_PREF), + true, + `${CM_PREF} has been set to true` + ); + break; + case "-cm": + is( + Services.prefs.getBoolPref(CM_PREF), + false, + `${CM_PREF} has been set to false` + ); + break; + case "emailTP": + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + true, + `${EMAIL_TP_PREF} has been set to true` + ); + break; + case "-emailTP": + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} has been set to false` + ); + break; + case "emailTPPrivate": + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} has been set to true` + ); + break; + case "-emailTPPrivate": + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + false, + `${EMAIL_TP_PBM_PREF} has been set to false` + ); + break; + case "lvl2": + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + true, + `${CM_PREF} has been set to true` + ); + break; + case "-lvl2": + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + false, + `${CM_PREF} has been set to false` + ); + break; + case "rp": + is( + Services.prefs.getBoolPref(REFERRER_PREF), + true, + `${REFERRER_PREF} has been set to true` + ); + break; + case "-rp": + is( + Services.prefs.getBoolPref(REFERRER_PREF), + false, + `${REFERRER_PREF} has been set to false` + ); + break; + case "rpTop": + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + true, + `${REFERRER_TOP_PREF} has been set to true` + ); + break; + case "-rpTop": + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + false, + `${REFERRER_TOP_PREF} has been set to false` + ); + break; + case "ocsp": + is( + Services.prefs.getBoolPref(OCSP_PREF), + true, + `${OCSP_PREF} has been set to true` + ); + break; + case "-ocsp": + is( + Services.prefs.getBoolPref(OCSP_PREF), + false, + `${OCSP_PREF} has been set to false` + ); + break; + case "qps": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + true, + `${QUERY_PARAM_STRIP_PREF} has been set to true` + ); + break; + case "-qps": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + false, + `${QUERY_PARAM_STRIP_PREF} has been set to false` + ); + break; + case "qpsPBM": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + true, + `${QUERY_PARAM_STRIP_PBM_PREF} has been set to true` + ); + break; + case "-qpsPBM": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + false, + `${QUERY_PARAM_STRIP_PBM_PREF} has been set to false` + ); + break; + case "cookieBehavior0": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_ACCEPT}` + ); + break; + case "cookieBehavior1": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}` + ); + break; + case "cookieBehavior2": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}` + ); + break; + case "cookieBehavior3": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}` + ); + break; + case "cookieBehavior4": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}` + ); + break; + case "cookieBehavior5": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + break; + case "cookieBehaviorPBM0": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_ACCEPT}` + ); + break; + case "cookieBehaviorPBM1": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}` + ); + break; + case "cookieBehaviorPBM2": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}` + ); + break; + case "cookieBehaviorPBM3": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}` + ); + break; + case "cookieBehaviorPBM4": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}` + ); + break; + case "cookieBehaviorPBM5": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + break; + default: + ok(false, "unknown option was added to the strict pref"); + break; + } + } + + gBrowser.removeCurrentTab(); +}); + +// Tests that the content blocking "Custom" category behaves as expected. +add_task(async function testContentBlockingCustomCategory() { + let untouchedPrefs = [ + TP_PREF, + TP_PBM_PREF, + NCB_PREF, + NCBP_PREF, + FP_PREF, + STP_PREF, + CM_PREF, + REFERRER_PREF, + REFERRER_TOP_PREF, + OCSP_PREF, + QUERY_PARAM_STRIP_PREF, + QUERY_PARAM_STRIP_PBM_PREF, + ]; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + let strictRadioOption = doc.getElementById("strictRadio"); + let standardRadioOption = doc.getElementById("standardRadio"); + let customRadioOption = doc.getElementById("customRadio"); + let defaults = new Preferences({ defaultBranch: true }); + + standardRadioOption.click(); + await TestUtils.waitForCondition( + () => !Services.prefs.prefHasUserValue(TP_PREF) + ); + + customRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "custom" + ); + + // The custom option will only force change of some prefs, like CAT_PREF. All + // other prefs should remain as they were for standard. + for (let pref of untouchedPrefs) { + ok( + !Services.prefs.prefHasUserValue(pref), + `the pref ${pref} remains as default value` + ); + } + + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + strictRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + + // Changing the following prefs should necessarily set CAT_PREF to "custom" + for (let pref of [ + FP_PREF, + STP_PREF, + CM_PREF, + TP_PREF, + TP_PBM_PREF, + REFERRER_PREF, + REFERRER_TOP_PREF, + OCSP_PREF, + QUERY_PARAM_STRIP_PREF, + QUERY_PARAM_STRIP_PBM_PREF, + ]) { + Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref)); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "custom" + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + strictRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + } + + // Changing the NCB_PREF should necessarily set CAT_PREF to "custom" + let defaultNCB = defaults.get(NCB_PREF); + let nonDefaultNCB; + switch (defaultNCB) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + default: + ok( + false, + "Unexpected default value found for " + NCB_PREF + ": " + defaultNCB + ); + break; + } + Services.prefs.setIntPref(NCB_PREF, nonDefaultNCB); + await TestUtils.waitForCondition(() => + Services.prefs.prefHasUserValue(NCB_PREF) + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + strictRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + + // Changing the NCBP_PREF should necessarily set CAT_PREF to "custom" + let defaultNCBP = defaults.get(NCBP_PREF); + let nonDefaultNCBP; + switch (defaultNCBP) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + default: + ok( + false, + "Unexpected default value found for " + NCBP_PREF + ": " + defaultNCBP + ); + break; + } + Services.prefs.setIntPref(NCBP_PREF, nonDefaultNCBP); + await TestUtils.waitForCondition(() => + Services.prefs.prefHasUserValue(NCBP_PREF) + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + for (let pref of untouchedPrefs) { + SpecialPowers.clearUserPref(pref); + } + + gBrowser.removeCurrentTab(); +}); + +function checkControlState(doc, controls, enabled) { + for (let selector of controls) { + for (let control of doc.querySelectorAll(selector)) { + if (enabled) { + ok(!control.hasAttribute("disabled"), `${selector} is enabled.`); + } else { + is( + control.getAttribute("disabled"), + "true", + `${selector} is disabled.` + ); + } + } + } +} + +// Checks that the menulists for tracking protection and cookie blocking are disabled when all TP prefs are off. +add_task(async function testContentBlockingDependentTPControls() { + SpecialPowers.pushPrefEnv({ + set: [ + [TP_PREF, false], + [TP_PBM_PREF, false], + [NCB_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT], + [CAT_PREF, "custom"], + ], + }); + + let disabledControls = ["#trackingProtectionMenu", "#blockCookiesMenu"]; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + checkControlState(doc, disabledControls, false); + + gBrowser.removeCurrentTab(); +}); + +// Checks that disabling tracking protection also disables email tracking protection. +add_task(async function testDisableTPCheckBoxDisablesEmailTP() { + SpecialPowers.pushPrefEnv({ + set: [ + [TP_PREF, false], + [TP_PBM_PREF, true], + [EMAIL_TP_PREF, false], + [EMAIL_TP_PBM_PREF, true], + [CAT_PREF, "custom"], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + // Click the checkbox to disable TP and check if this disables Email TP. + let tpCheckbox = doc.getElementById( + "contentBlockingTrackingProtectionCheckbox" + ); + + // Verify the initial check state of the tracking protection checkbox. + is( + tpCheckbox.getAttribute("checked"), + "true", + "Tracking protection checkbox is checked initially" + ); + + tpCheckbox.click(); + + // Verify the checkbox is unchecked after clicking. + is( + tpCheckbox.getAttribute("checked"), + "", + "Tracking protection checkbox is unchecked" + ); + + // Verify the pref states. + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} has been set to false` + ); + + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + false, + `${EMAIL_TP_PBM_PREF} has been set to false` + ); + + gBrowser.removeCurrentTab(); +}); + +// Checks that the email tracking prefs set properly with tracking protection +// drop downs. +add_task(async function testTPMenuForEmailTP() { + SpecialPowers.pushPrefEnv({ + set: [ + [TP_PREF, false], + [TP_PBM_PREF, true], + [EMAIL_TP_PREF, false], + [EMAIL_TP_PBM_PREF, true], + [CAT_PREF, "custom"], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let menu = doc.querySelector("#trackingProtectionMenu"); + let always = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=always]" + ); + let privateElement = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=private]" + ); + + // Click the always option on the tracking protection drop down. + menu.selectedItem = always; + always.click(); + + // Verify the pref states. + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + true, + `${EMAIL_TP_PREF} has been set to true` + ); + + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} has been set to true` + ); + + // Click the private-only option on the tracking protection drop down. + menu.selectedItem = privateElement; + privateElement.click(); + + // Verify the pref states. + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} has been set to false` + ); + + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} has been set to true` + ); + + gBrowser.removeCurrentTab(); +}); + +// Checks that social media trackers, cryptomining and fingerprinting visibility +// can be controlled via pref. +add_task(async function testCustomOptionsVisibility() { + Services.prefs.setBoolPref( + "browser.contentblocking.cryptomining.preferences.ui.enabled", + false + ); + Services.prefs.setBoolPref( + "browser.contentblocking.fingerprinting.preferences.ui.enabled", + false + ); + Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + false + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + let cryptominersOption = doc.getElementById( + "contentBlockingCryptominersOption" + ); + let fingerprintersOption = doc.getElementById( + "contentBlockingFingerprintersOption" + ); + + ok(cryptominersOption.hidden, "Cryptomining is hidden"); + ok(fingerprintersOption.hidden, "Fingerprinting is hidden"); + + gBrowser.removeCurrentTab(); + + Services.prefs.setBoolPref( + "browser.contentblocking.cryptomining.preferences.ui.enabled", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + cryptominersOption = doc.getElementById("contentBlockingCryptominersOption"); + fingerprintersOption = doc.getElementById( + "contentBlockingFingerprintersOption" + ); + + ok(!cryptominersOption.hidden, "Cryptomining is shown"); + ok(fingerprintersOption.hidden, "Fingerprinting is hidden"); + + gBrowser.removeCurrentTab(); + + Services.prefs.setBoolPref( + "browser.contentblocking.fingerprinting.preferences.ui.enabled", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + cryptominersOption = doc.getElementById("contentBlockingCryptominersOption"); + fingerprintersOption = doc.getElementById( + "contentBlockingFingerprintersOption" + ); + + ok(!cryptominersOption.hidden, "Cryptomining is shown"); + ok(!fingerprintersOption.hidden, "Fingerprinting is shown"); + + gBrowser.removeCurrentTab(); + + // Social media trackers UI should be hidden + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + let socialTrackingUI = [...doc.querySelectorAll(".social-media-option")]; + + ok( + socialTrackingUI.every(el => el.hidden), + "All Social media tracker UI instances are hidden" + ); + + gBrowser.removeCurrentTab(); + + // Social media trackers UI should be visible + Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + socialTrackingUI = [...doc.querySelectorAll(".social-media-option")]; + + ok( + !socialTrackingUI.every(el => el.hidden), + "All Social media tracker UI instances are visible" + ); + + gBrowser.removeCurrentTab(); + + Services.prefs.clearUserPref( + "browser.contentblocking.cryptomining.preferences.ui.enabled" + ); + Services.prefs.clearUserPref( + "browser.contentblocking.fingerprinting.preferences.ui.enabled" + ); + Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled"); +}); + +// Checks that adding a custom enterprise policy will put the user in the custom category. +// Other categories will be disabled. +add_task(async function testPolicyCategorization() { + Services.prefs.setStringPref(CAT_PREF, "standard"); + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} starts on standard` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} starts with the default value` + ); + PoliciesPrefTracker.start(); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + EnableTrackingProtection: { + Value: true, + }, + }, + }); + EnterprisePolicyTesting.checkPolicyPref(TP_PREF, true, false); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + Services.prefs.setStringPref(CAT_PREF, "standard"); + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} starts on standard` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} starts with the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} starts with the default value` + ); + + let uiUpdatedPromise = TestUtils.topicObserved("privacy-pane-tp-ui-updated"); + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + Cookies: { + AcceptThirdParty: "never", + Locked: true, + }, + }, + }); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await uiUpdatedPromise; + + EnterprisePolicyTesting.checkPolicyPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + true + ); + EnterprisePolicyTesting.checkPolicyPref( + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + true + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + let doc = gBrowser.contentDocument; + let strictRadioOption = doc.getElementById("strictRadio"); + let standardRadioOption = doc.getElementById("standardRadio"); + is(strictRadioOption.disabled, true, "the strict option is disabled"); + is(standardRadioOption.disabled, true, "the standard option is disabled"); + + gBrowser.removeCurrentTab(); + + // Cleanup after this particular test. + if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) { + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + Cookies: { + Locked: false, + }, + }, + }); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); + } + is( + Services.policies.status, + Ci.nsIEnterprisePolicies.INACTIVE, + "Engine is inactive at the end of the test" + ); + + EnterprisePolicyTesting.resetRunOnceState(); + PoliciesPrefTracker.stop(); +}); + +// Tests that changing a content blocking pref shows the content blocking warning +// to reload tabs to apply changes. +add_task(async function testContentBlockingReloadWarning() { + Services.prefs.setStringPref(CAT_PREF, "standard"); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + let reloadWarnings = [ + ...doc.querySelectorAll(".content-blocking-warning.reload-tabs"), + ]; + let allHidden = reloadWarnings.every(el => el.hidden); + ok(allHidden, "all of the warnings to reload tabs are initially hidden"); + + Services.prefs.setStringPref(CAT_PREF, "strict"); + + let strictWarning = doc.querySelector( + "#contentBlockingOptionStrict .content-blocking-warning.reload-tabs" + ); + ok( + !BrowserTestUtils.is_hidden(strictWarning), + "The warning in the strict section should be showing" + ); + + Services.prefs.setStringPref(CAT_PREF, "standard"); + gBrowser.removeCurrentTab(); +}); + +// Tests that changing a content blocking pref does not show the content blocking warning +// if it is the only tab. +add_task(async function testContentBlockingReloadWarningSingleTab() { + Services.prefs.setStringPref(CAT_PREF, "standard"); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, PRIVACY_PAGE); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + PRIVACY_PAGE + ); + + let reloadWarnings = [ + ...gBrowser.contentDocument.querySelectorAll( + ".content-blocking-warning.reload-tabs" + ), + ]; + ok(reloadWarnings.length, "must have at least one reload warning"); + ok( + reloadWarnings.every(el => el.hidden), + "all of the warnings to reload tabs are initially hidden" + ); + + is(BrowserWindowTracker.windowCount, 1, "There is only one window open"); + is(gBrowser.tabs.length, 1, "There is only one tab open"); + Services.prefs.setStringPref(CAT_PREF, "strict"); + + ok( + reloadWarnings.every(el => el.hidden), + "all of the warnings to reload tabs are still hidden" + ); + Services.prefs.setStringPref(CAT_PREF, "standard"); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:newtab"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); +}); + +// Checks that the reload tabs message reloads all tabs except the active tab. +add_task(async function testReloadTabsMessage() { + Services.prefs.setStringPref(CAT_PREF, "strict"); + let exampleTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com" + ); + let examplePinnedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com" + ); + gBrowser.pinTab(examplePinnedTab); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + let standardWarning = doc.querySelector( + "#contentBlockingOptionStandard .content-blocking-warning.reload-tabs" + ); + let standardReloadButton = doc.querySelector( + "#contentBlockingOptionStandard .reload-tabs-button" + ); + + Services.prefs.setStringPref(CAT_PREF, "standard"); + ok( + !BrowserTestUtils.is_hidden(standardWarning), + "The warning in the standard section should be showing" + ); + + let exampleTabBrowserDiscardedPromise = BrowserTestUtils.waitForEvent( + exampleTab, + "TabBrowserDiscarded" + ); + let examplePinnedTabLoadPromise = BrowserTestUtils.browserLoaded( + examplePinnedTab.linkedBrowser + ); + standardReloadButton.click(); + // The pinned example page had a load event + await examplePinnedTabLoadPromise; + // The other one had its browser discarded + await exampleTabBrowserDiscardedPromise; + + ok( + BrowserTestUtils.is_hidden(standardWarning), + "The warning in the standard section should have hidden after being clicked" + ); + + // cleanup + Services.prefs.setStringPref(CAT_PREF, "standard"); + gBrowser.removeTab(exampleTab); + gBrowser.removeTab(examplePinnedTab); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_contentblocking_categories.js b/browser/components/preferences/tests/browser_contentblocking_categories.js new file mode 100644 index 0000000000..3b9e16a2fe --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking_categories.js @@ -0,0 +1,487 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env webextensions */ + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +const TP_PREF = "privacy.trackingprotection.enabled"; +const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled"; +const NCB_PREF = "network.cookie.cookieBehavior"; +const NCBP_PREF = "network.cookie.cookieBehavior.pbmode"; +const CAT_PREF = "browser.contentblocking.category"; +const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled"; +const CM_PREF = "privacy.trackingprotection.cryptomining.enabled"; +const STP_PREF = "privacy.trackingprotection.socialtracking.enabled"; +const EMAIL_TP_PREF = "privacy.trackingprotection.emailtracking.enabled"; +const EMAIL_TP_PBM_PREF = + "privacy.trackingprotection.emailtracking.pbmode.enabled"; +const LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled"; +const REFERRER_PREF = "network.http.referer.disallowCrossSiteRelaxingDefault"; +const REFERRER_TOP_PREF = + "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation"; +const OCSP_PREF = "privacy.partition.network_state.ocsp_cache"; +const QUERY_PARAM_STRIP_PREF = "privacy.query_stripping.enabled"; +const QUERY_PARAM_STRIP_PBM_PREF = "privacy.query_stripping.enabled.pbmode"; +const STRICT_DEF_PREF = "browser.contentblocking.features.strict"; + +// Tests that the content blocking standard category definition is based on the default settings of +// the content blocking prefs. +// Changing the definition does not remove the user from the category. +add_task(async function testContentBlockingStandardDefinition() { + Services.prefs.setStringPref(CAT_PREF, "strict"); + Services.prefs.setStringPref(CAT_PREF, "standard"); + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} starts on standard` + ); + + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PBM_PREF), + `${TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(FP_PREF), + `${FP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(CM_PREF), + `${CM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(STP_PREF), + `${STP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PREF), + `${EMAIL_TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF), + `${EMAIL_TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_PREF), + `${REFERRER_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF), + `${REFERRER_TOP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(OCSP_PREF), + `${OCSP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF), + `${QUERY_PARAM_STRIP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF), + `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value` + ); + + let defaults = Services.prefs.getDefaultBranch(""); + let originalTP = defaults.getBoolPref(TP_PREF); + let originalTPPBM = defaults.getBoolPref(TP_PBM_PREF); + let originalFP = defaults.getBoolPref(FP_PREF); + let originalCM = defaults.getBoolPref(CM_PREF); + let originalSTP = defaults.getBoolPref(STP_PREF); + let originalEmailTP = defaults.getBoolPref(EMAIL_TP_PREF); + let originalEmailTPPBM = defaults.getBoolPref(EMAIL_TP_PBM_PREF); + let originalNCB = defaults.getIntPref(NCB_PREF); + let originalNCBP = defaults.getIntPref(NCBP_PREF); + let originalLEVEL2 = defaults.getBoolPref(LEVEL2_PREF); + let originalREFERRER = defaults.getBoolPref(REFERRER_PREF); + let originalREFERRERTOP = defaults.getBoolPref(REFERRER_TOP_PREF); + let originalOCSP = defaults.getBoolPref(OCSP_PREF); + let originalQueryParamStrip = defaults.getBoolPref(QUERY_PARAM_STRIP_PREF); + let originalQueryParamStripPBM = defaults.getBoolPref( + QUERY_PARAM_STRIP_PBM_PREF + ); + + let nonDefaultNCB; + switch (originalNCB) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + default: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + } + let nonDefaultNCBP; + switch (originalNCBP) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + default: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + } + defaults.setIntPref(NCB_PREF, nonDefaultNCB); + defaults.setIntPref(NCBP_PREF, nonDefaultNCBP); + defaults.setBoolPref(TP_PREF, !originalTP); + defaults.setBoolPref(TP_PBM_PREF, !originalTPPBM); + defaults.setBoolPref(FP_PREF, !originalFP); + defaults.setBoolPref(CM_PREF, !originalCM); + defaults.setBoolPref(CM_PREF, !originalSTP); + defaults.setBoolPref(EMAIL_TP_PREF, !originalEmailTP); + defaults.setBoolPref(EMAIL_TP_PBM_PREF, !originalEmailTPPBM); + defaults.setIntPref(NCB_PREF, !originalNCB); + defaults.setBoolPref(LEVEL2_PREF, !originalLEVEL2); + defaults.setBoolPref(REFERRER_PREF, !originalREFERRER); + defaults.setBoolPref(REFERRER_TOP_PREF, !originalREFERRERTOP); + defaults.setBoolPref(OCSP_PREF, !originalOCSP); + defaults.setBoolPref(QUERY_PARAM_STRIP_PREF, !originalQueryParamStrip); + defaults.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, !originalQueryParamStripPBM); + + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PBM_PREF), + `${TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(FP_PREF), + `${FP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(CM_PREF), + `${CM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(STP_PREF), + `${STP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PREF), + `${EMAIL_TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF), + `${EMAIL_TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_PREF), + `${REFERRER_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF), + `${REFERRER_TOP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(OCSP_PREF), + `${OCSP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF), + `${QUERY_PARAM_STRIP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF), + `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value` + ); + + // cleanup + defaults.setIntPref(NCB_PREF, originalNCB); + defaults.setBoolPref(TP_PREF, originalTP); + defaults.setBoolPref(TP_PBM_PREF, originalTPPBM); + defaults.setBoolPref(FP_PREF, originalFP); + defaults.setBoolPref(CM_PREF, originalCM); + defaults.setBoolPref(STP_PREF, originalSTP); + defaults.setBoolPref(EMAIL_TP_PREF, originalEmailTP); + defaults.setBoolPref(EMAIL_TP_PBM_PREF, originalEmailTPPBM); + defaults.setIntPref(NCB_PREF, originalNCB); + defaults.setIntPref(NCBP_PREF, originalNCBP); + defaults.setBoolPref(LEVEL2_PREF, originalLEVEL2); + defaults.setBoolPref(REFERRER_PREF, originalREFERRER); + defaults.setBoolPref(REFERRER_TOP_PREF, originalREFERRERTOP); + defaults.setBoolPref(OCSP_PREF, originalOCSP); + defaults.setBoolPref(QUERY_PARAM_STRIP_PREF, originalQueryParamStrip); + defaults.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, originalQueryParamStripPBM); +}); + +// Tests that the content blocking strict category definition changes the behavior +// of the strict category pref and all prefs it controls. +// Changing the definition does not remove the user from the category. +add_task(async function testContentBlockingStrictDefinition() { + let defaults = Services.prefs.getDefaultBranch(""); + let originalStrictPref = defaults.getStringPref(STRICT_DEF_PREF); + defaults.setStringPref( + STRICT_DEF_PREF, + "tp,tpPrivate,fp,cm,cookieBehavior0,cookieBehaviorPBM0,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp,qps,qpsPBM" + ); + Services.prefs.setStringPref(CAT_PREF, "strict"); + is( + Services.prefs.getStringPref(CAT_PREF), + "strict", + `${CAT_PREF} has changed to strict` + ); + + ok( + !Services.prefs.prefHasUserValue(STRICT_DEF_PREF), + `We changed the default value of ${STRICT_DEF_PREF}` + ); + is( + Services.prefs.getStringPref(STRICT_DEF_PREF), + "tp,tpPrivate,fp,cm,cookieBehavior0,cookieBehaviorPBM0,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp,qps,qpsPBM", + `${STRICT_DEF_PREF} changed to what we set.` + ); + + is( + Services.prefs.getBoolPref(TP_PREF), + true, + `${TP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + true, + `${TP_PBM_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(FP_PREF), + true, + `${CM_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(CM_PREF), + true, + `${CM_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(STP_PREF), + true, + `${STP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + true, + `${EMAIL_TP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} pref has been set to true` + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCB_PREF} has been set to BEHAVIOR_ACCEPT` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCBP_PREF} has been set to BEHAVIOR_ACCEPT` + ); + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + true, + `${LEVEL2_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(REFERRER_PREF), + true, + `${REFERRER_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + true, + `${REFERRER_TOP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(OCSP_PREF), + true, + `${OCSP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + true, + `${QUERY_PARAM_STRIP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + true, + `${QUERY_PARAM_STRIP_PBM_PREF} pref has been set to true` + ); + + // Note, if a pref is not listed it will use the default value, however this is only meant as a + // backup if a mistake is made. The UI will not respond correctly. + defaults.setStringPref(STRICT_DEF_PREF, ""); + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PBM_PREF), + `${TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(FP_PREF), + `${FP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(CM_PREF), + `${CM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(STP_PREF), + `${STP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PREF), + `${EMAIL_TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF), + `${EMAIL_TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_PREF), + `${REFERRER_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF), + `${REFERRER_TOP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(OCSP_PREF), + `${OCSP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF), + `${QUERY_PARAM_STRIP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF), + `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value` + ); + + defaults.setStringPref( + STRICT_DEF_PREF, + "-tpPrivate,-fp,-cm,-tp,cookieBehavior3,cookieBehaviorPBM2,-stp,-emailTP,-emailTPPrivate,-lvl2,-rp,-ocsp,-qps,-qpsPBM" + ); + is( + Services.prefs.getBoolPref(TP_PREF), + false, + `${TP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + false, + `${TP_PBM_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(FP_PREF), + false, + `${FP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(CM_PREF), + false, + `${CM_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(STP_PREF), + false, + `${STP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + false, + `${EMAIL_TP_PBM_PREF} pref has been set to false` + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCB_PREF} has been set to BEHAVIOR_REJECT_TRACKER` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCBP_PREF} has been set to BEHAVIOR_REJECT` + ); + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + false, + `${LEVEL2_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(REFERRER_PREF), + false, + `${REFERRER_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + false, + `${REFERRER_TOP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(OCSP_PREF), + false, + `${OCSP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + false, + `${QUERY_PARAM_STRIP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + false, + `${QUERY_PARAM_STRIP_PBM_PREF} pref has been set to false` + ); + + // cleanup + defaults.setStringPref(STRICT_DEF_PREF, originalStrictPref); + Services.prefs.setStringPref(CAT_PREF, "standard"); +}); diff --git a/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js b/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js new file mode 100644 index 0000000000..249a42317a --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the TCP info box in the ETP standard section of about:preferences#privacy. + */ + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const CAT_PREF = "browser.contentblocking.category"; + +const LEARN_MORE_URL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "total-cookie-protection"; + +const { + BEHAVIOR_REJECT_TRACKER, + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, +} = Ci.nsICookieService; + +async function testTCPSection({ dFPIEnabled }) { + info( + "Testing TCP preferences section in standard " + + JSON.stringify({ dFPIEnabled }) + ); + + // In order to test the "standard" category we need to set the default value + // for the cookie behavior pref. A user value would get cleared as soon as we + // switch to "standard". + Services.prefs + .getDefaultBranch("") + .setIntPref( + COOKIE_BEHAVIOR_PREF, + dFPIEnabled + ? BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + : BEHAVIOR_REJECT_TRACKER + ); + + // Setting to standard category explicitly, since changing the default cookie + // behavior still switches us to custom initially. + await SpecialPowers.pushPrefEnv({ + set: [[CAT_PREF, "standard"]], + }); + + const uiEnabled = + Services.prefs.getIntPref(COOKIE_BEHAVIOR_PREF) == + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let standardRadioOption = doc.getElementById("standardRadio"); + let strictRadioOption = doc.getElementById("strictRadio"); + let customRadioOption = doc.getElementById("customRadio"); + + ok(standardRadioOption.selected, "Standard category is selected"); + + let etpStandardTCPBox = doc.getElementById("etpStandardTCPBox"); + is( + BrowserTestUtils.is_visible(etpStandardTCPBox), + uiEnabled, + `TCP section in standard is ${uiEnabled ? " " : "not "}visible.` + ); + + if (uiEnabled) { + // If visible, test the TCP section elements. + let learnMoreLink = etpStandardTCPBox.querySelector("#tcp-learn-more-link"); + ok(learnMoreLink, "Should have a learn more link"); + BrowserTestUtils.is_visible( + learnMoreLink, + "Learn more link should be visible." + ); + ok( + learnMoreLink.href && !learnMoreLink.href.startsWith("about:blank"), + "Learn more link should be valid." + ); + is( + learnMoreLink.href, + LEARN_MORE_URL, + "Learn more link should have the correct target." + ); + + let description = etpStandardTCPBox.querySelector(".tail-with-learn-more"); + ok(description, "Should have a description element."); + BrowserTestUtils.is_visible(description, "Description should be visible."); + + let title = etpStandardTCPBox.querySelector( + ".content-blocking-warning-title" + ); + ok(title, "Should have a title element."); + BrowserTestUtils.is_visible(title, "Title should be visible."); + } + + info("Switch to ETP strict."); + let categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "strict"); + strictRadioOption.click(); + await categoryPrefChange; + ok( + !BrowserTestUtils.is_visible(etpStandardTCPBox), + "When strict is selected TCP UI is not visible." + ); + + info("Switch to ETP custom."); + categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "custom"); + customRadioOption.click(); + await categoryPrefChange; + ok( + !BrowserTestUtils.is_visible(etpStandardTCPBox), + "When custom is selected TCP UI is not visible." + ); + + info("Switch back to standard and ensure we show the TCP UI again."); + categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "standard"); + standardRadioOption.click(); + await categoryPrefChange; + is( + BrowserTestUtils.is_visible(etpStandardTCPBox), + uiEnabled, + `TCP section in standard is ${uiEnabled ? " " : "not "}visible.` + ); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); + Services.prefs.setStringPref(CAT_PREF, "standard"); +} + +add_setup(async function () { + // Register cleanup function to restore default cookie behavior. + const defaultPrefs = Services.prefs.getDefaultBranch(""); + const previousDefaultCB = defaultPrefs.getIntPref(COOKIE_BEHAVIOR_PREF); + + registerCleanupFunction(function () { + defaultPrefs.setIntPref(COOKIE_BEHAVIOR_PREF, previousDefaultCB); + }); +}); + +// Clients which don't have dFPI enabled should not see the +// preferences section. +add_task(async function test_dfpi_disabled() { + await testTCPSection({ dFPIEnabled: false }); +}); + +add_task(async function test_dfpi_enabled() { + await testTCPSection({ dFPIEnabled: true }); +}); diff --git a/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js new file mode 100644 index 0000000000..1b70170ddc --- /dev/null +++ b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js @@ -0,0 +1,299 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +if (AppConstants.TSAN || AppConstants.DEBUG) { + requestLongerTimeout(2); +} + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +async function openCookiesDialog(doc) { + let cookieExceptionsButton = doc.getElementById("cookieExceptions"); + ok(cookieExceptionsButton, "cookieExceptionsButton found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + cookieExceptionsButton.click(); + let dialog = await dialogPromise; + return dialog; +} + +function checkCookiesDialog(dialog) { + ok(dialog, "dialog loaded"); + let buttonIds = ["removePermission", "removeAllPermissions"]; + + for (let buttonId of buttonIds) { + let button = dialog.document.getElementById(buttonId); + ok(button, `${buttonId} found`); + } + + let dialogEl = dialog.document.querySelector("dialog"); + let acceptBtn = dialogEl.getButton("accept"); + let cancelBtn = dialogEl.getButton("cancel"); + + ok(!acceptBtn.hidden, "acceptButton found"); + ok(!cancelBtn.hidden, "cancelButton found"); +} + +function addNewPermission(websiteAddress, dialog) { + let url = dialog.document.getElementById("url"); + let buttonDialog = dialog.document.getElementById("btnBlock"); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let currentPermissions = permissionsBox.itemCount; + + url.value = websiteAddress; + url.dispatchEvent(new Event("input", { bubbles: true })); + is( + buttonDialog.hasAttribute("disabled"), + false, + "When the user add an url the button should be clickable" + ); + buttonDialog.click(); + + is( + permissionsBox.itemCount, + currentPermissions + 1, + "Website added in url should be in the list" + ); +} + +async function cleanList(dialog) { + let removeAllButton = dialog.document.getElementById("removeAllPermissions"); + if (!removeAllButton.hasAttribute("disabled")) { + removeAllButton.click(); + } +} + +function addData(websites, dialog) { + for (let website of websites) { + addNewPermission(website, dialog); + } +} + +function deletePermission(permission, dialog) { + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let elements = permissionsBox.getElementsByAttribute("origin", permission); + is(elements.length, 1, "It should find only one entry"); + permissionsBox.selectItem(elements[0]); + let removePermissionButton = + dialog.document.getElementById("removePermission"); + is( + removePermissionButton.hasAttribute("disabled"), + false, + "The button should be clickable to remove selected item" + ); + removePermissionButton.click(); +} + +function save(dialog) { + let saveButton = dialog.document.querySelector("dialog").getButton("accept"); + saveButton.click(); +} + +function cancel(dialog) { + let cancelButton = dialog.document + .querySelector("dialog") + .getButton("cancel"); + ok(!cancelButton.hidden, "cancelButton found"); + cancelButton.click(); +} + +async function checkExpected(expected, doc) { + let dialog = await openCookiesDialog(doc); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + + is( + permissionsBox.itemCount, + expected.length, + `There should be ${expected.length} elements in the list` + ); + + for (let website of expected) { + let elements = permissionsBox.getElementsByAttribute("origin", website); + is(elements.length, 1, "It should find only one entry"); + } + return dialog; +} + +async function runTest(test, websites, doc) { + let dialog = await openCookiesDialog(doc); + checkCookiesDialog(dialog); + + if (test.needPreviousData) { + addData(websites, dialog); + save(dialog); + dialog = await openCookiesDialog(doc); + } + + for (let step of test.steps) { + switch (step) { + case "addNewPermission": + addNewPermission(test.newData, dialog); + break; + case "deletePermission": + deletePermission(test.newData, dialog); + break; + case "deleteAllPermission": + await cleanList(dialog); + break; + case "save": + save(dialog); + break; + case "cancel": + cancel(dialog); + break; + case "openPane": + dialog = await openCookiesDialog(doc); + break; + default: + // code block + } + } + dialog = await checkExpected(test.expected, doc); + await cleanList(dialog); + save(dialog); +} + +add_task(async function checkPermissions() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + let websites = ["http://test1.com", "http://test2.com"]; + + let tests = [ + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "save"], + expected: ["https://mytest.com"], // when open the pane again it should find this in the list + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "cancel"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deletePermission", "save"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deletePermission", "cancel"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deletePermission", + "save", + ], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deletePermission", + "cancel", + ], + expected: ["https://mytest.com"], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deleteAllPermission", "save"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deleteAllPermission", "cancel"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deleteAllPermission", + "save", + ], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deleteAllPermission", + "cancel", + ], + expected: ["https://mytest.com"], + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["deleteAllPermission", "save"], + expected: [], + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["deleteAllPermission", "cancel"], + expected: websites, + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["addNewPermission", "save"], + expected: (function () { + let result = websites.slice(); + result.push("https://mytest.com"); + return result; + })(), + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["addNewPermission", "cancel"], + expected: websites, + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deleteAllPermission", + "addNewPermission", + "save", + ], + expected: ["https://mytest.com"], + }, + ]; + + for (let test of tests) { + await runTest(test, websites, doc); + } + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_cookies_exceptions.js b/browser/components/preferences/tests/browser_cookies_exceptions.js new file mode 100644 index 0000000000..d2d538a48a --- /dev/null +++ b/browser/components/preferences/tests/browser_cookies_exceptions.js @@ -0,0 +1,568 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +requestLongerTimeout(3); + +add_task(async function testAllow() { + await runTest( + async (params, observeAllPromise, apply) => { + assertListContents(params, []); + + params.url.value = "test.com"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://test.com", params.allowL10nId], + ["https://test.com", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testBlock() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "test.com"; + params.btnBlock.doCommand(); + + assertListContents(params, [ + ["http://test.com", params.denyL10nId], + ["https://test.com", params.denyL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + { + type: "cookie", + origin: "https://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowAgain() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "test.com"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://test.com", params.allowL10nId], + ["https://test.com", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testRemove() { + await runTest( + async (params, observeAllPromise, apply) => { + while (params.richlistbox.itemCount) { + params.richlistbox.selectedIndex = 0; + params.btnRemove.doCommand(); + } + assertListContents(params, []); + + apply(); + await observeAllPromise; + }, + params => { + let richlistItems = params.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "cookie", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); +}); + +add_task(async function testAdd() { + await runTest( + async (params, observeAllPromise, apply) => { + let uri = Services.io.newURI("http://test.com"); + PermissionTestUtils.add( + uri, + "popup", + Ci.nsIPermissionManager.DENY_ACTION + ); + + info("Adding unrelated permission should not change display."); + assertListContents(params, []); + + apply(); + await observeAllPromise; + + PermissionTestUtils.remove(uri, "popup"); + }, + params => { + return [ + { + type: "popup", + origin: "http://test.com", + data: "added", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "https://test.com:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["https://test.com:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "https://test.com:12345", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testBlockHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "https://test.com:12345"; + params.btnBlock.doCommand(); + + assertListContents(params, [ + ["https://test.com:12345", params.denyL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "https://test.com:12345", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowAgainHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "https://test.com:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["https://test.com:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "https://test.com:12345", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testRemoveHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + while (params.richlistbox.itemCount) { + params.richlistbox.selectedIndex = 0; + params.btnRemove.doCommand(); + } + + assertListContents(params, []); + + apply(); + await observeAllPromise; + }, + params => { + let richlistItems = params.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "cookie", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); +}); + +add_task(async function testAllowPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "localhost:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://localhost:12345", params.allowL10nId], + ["https://localhost:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://localhost:12345", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://localhost:12345", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testBlockPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "localhost:12345"; + params.btnBlock.doCommand(); + + assertListContents(params, [ + ["http://localhost:12345", params.denyL10nId], + ["https://localhost:12345", params.denyL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + { + type: "cookie", + origin: "https://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowAgainPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "localhost:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://localhost:12345", params.allowL10nId], + ["https://localhost:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testRemovePort() { + await runTest( + async (params, observeAllPromise, apply) => { + while (params.richlistbox.itemCount) { + params.richlistbox.selectedIndex = 0; + params.btnRemove.doCommand(); + } + + assertListContents(params, []); + + apply(); + await observeAllPromise; + }, + params => { + let richlistItems = params.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "cookie", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); +}); + +add_task(async function testSort() { + await runTest( + async (params, observeAllPromise, apply) => { + // Sort by site name. + EventUtils.synthesizeMouseAtCenter( + params.doc.getElementById("siteCol"), + {}, + params.doc.defaultView + ); + + for (let URL of ["http://a", "http://z", "http://b"]) { + let URI = Services.io.newURI(URL); + PermissionTestUtils.add( + URI, + "cookie", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + } + + assertListContents(params, [ + ["http://a", params.allowL10nId], + ["http://b", params.allowL10nId], + ["http://z", params.allowL10nId], + ]); + + // Sort by site name in descending order. + EventUtils.synthesizeMouseAtCenter( + params.doc.getElementById("siteCol"), + {}, + params.doc.defaultView + ); + + assertListContents(params, [ + ["http://z", params.allowL10nId], + ["http://b", params.allowL10nId], + ["http://a", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + + for (let URL of ["http://a", "http://z", "http://b"]) { + let uri = Services.io.newURI(URL); + PermissionTestUtils.remove(uri, "cookie"); + } + }, + params => { + return [ + { + type: "cookie", + origin: "http://a", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "http://z", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "http://b", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testPrivateBrowsingSessionPermissionsAreHidden() { + await runTest( + async (params, observeAllPromise, apply) => { + assertListContents(params, []); + + let uri = Services.io.newURI("http://test.com"); + let privateBrowsingPrincipal = + Services.scriptSecurityManager.createContentPrincipal(uri, { + privateBrowsingId: 1, + }); + + // Add a session permission for private browsing. + PermissionTestUtils.add( + privateBrowsingPrincipal, + "cookie", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_SESSION + ); + + assertListContents(params, []); + + PermissionTestUtils.remove(uri, "cookie"); + }, + params => { + return []; + } + ); +}); + +function assertListContents(params, expected) { + Assert.equal(params.richlistbox.itemCount, expected.length); + + for (let i = 0; i < expected.length; i++) { + let website = expected[i][0]; + let elements = params.richlistbox.getElementsByAttribute("origin", website); + Assert.equal(elements.length, 1); // "It should find only one coincidence" + Assert.equal( + elements[0] + .querySelector(".website-capability-value") + .getAttribute("data-l10n-id"), + expected[i][1] + ); + } +} + +async function runTest(test, getObservances) { + registerCleanupFunction(function () { + Services.prefs.clearUserPref("privacy.history.custom"); + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let historyMode = doc.getElementById("historyMode"); + historyMode.value = "custom"; + historyMode.doCommand(); + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + doc.getElementById("cookieExceptions").doCommand(); + + let win = await promiseSubDialogLoaded; + + doc = win.document; + let params = { + doc, + richlistbox: doc.getElementById("permissionsBox"), + url: doc.getElementById("url"), + btnAllow: doc.getElementById("btnAllow"), + btnBlock: doc.getElementById("btnBlock"), + btnRemove: doc.getElementById("removePermission"), + allowL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIPermissionManager.ALLOW_ACTION + ), + denyL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIPermissionManager.DENY_ACTION + ), + allow: Ci.nsIPermissionManager.ALLOW_ACTION, + deny: Ci.nsIPermissionManager.DENY_ACTION, + }; + let btnApplyChanges = doc.querySelector("dialog").getButton("accept"); + let observances = getObservances(params); + let observeAllPromise = createObserveAllPromise(observances); + + await test(params, observeAllPromise, () => btnApplyChanges.doCommand()); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} diff --git a/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js b/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js new file mode 100644 index 0000000000..dc88e36cef --- /dev/null +++ b/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js @@ -0,0 +1,185 @@ +"use strict"; + +const CHECK_DEFAULT_INITIAL = Services.prefs.getBoolPref( + "browser.shell.checkDefaultBrowser" +); + +add_task(async function clicking_make_default_checks_alwaysCheck_checkbox() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + await test_with_mock_shellservice({ isDefault: false }, async function () { + let checkDefaultBrowserState = isDefault => { + let isDefaultPane = content.document.getElementById("isDefaultPane"); + let isNotDefaultPane = + content.document.getElementById("isNotDefaultPane"); + Assert.equal( + ContentTaskUtils.is_hidden(isDefaultPane), + !isDefault, + "The 'browser is default' pane should be hidden when browser is not default" + ); + Assert.equal( + ContentTaskUtils.is_hidden(isNotDefaultPane), + isDefault, + "The 'make default' pane should be hidden when browser is default" + ); + }; + + checkDefaultBrowserState(false); + + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok(!alwaysCheck.checked, "Always Check is unchecked by default"); + Assert.ok( + !Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "alwaysCheck pref should be false by default in test runs" + ); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + await ContentTaskUtils.waitForCondition( + () => alwaysCheck.checked, + "'Always Check' checkbox should get checked after clicking the 'Set Default' button" + ); + + Assert.ok( + alwaysCheck.checked, + "Clicking 'Make Default' checks the 'Always Check' checkbox" + ); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "Checking the checkbox should set the pref to true" + ); + Assert.ok( + alwaysCheck.disabled, + "'Always Check' checkbox is locked with default browser and alwaysCheck=true" + ); + checkDefaultBrowserState(true); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "checkDefaultBrowser pref is now enabled" + ); + }); + + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser"); +}); + +add_task(async function clicking_make_default_checks_alwaysCheck_checkbox() { + Services.prefs.lockPref("browser.shell.checkDefaultBrowser"); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + await test_with_mock_shellservice({ isDefault: false }, async function () { + let isDefaultPane = content.document.getElementById("isDefaultPane"); + let isNotDefaultPane = content.document.getElementById("isNotDefaultPane"); + Assert.ok( + ContentTaskUtils.is_hidden(isDefaultPane), + "The 'browser is default' pane should be hidden when not default" + ); + Assert.ok( + ContentTaskUtils.is_visible(isNotDefaultPane), + "The 'make default' pane should be visible when not default" + ); + + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok(alwaysCheck.disabled, "Always Check is disabled when locked"); + Assert.ok( + alwaysCheck.checked, + "Always Check is checked because defaultPref is true and pref is locked" + ); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "alwaysCheck pref should ship with 'true' by default" + ); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(isDefaultPane), + "Browser is now default" + ); + + Assert.ok( + alwaysCheck.checked, + "'Always Check' is still checked because it's locked" + ); + Assert.ok( + alwaysCheck.disabled, + "'Always Check is disabled because it's locked" + ); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "The pref is locked and so doesn't get changed" + ); + }); + + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + gBrowser.removeCurrentTab(); +}); + +add_task(async function make_default_disabled_until_prefs_are_loaded() { + // Testcase with Firefox not set as the default browser + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + await test_with_mock_shellservice({ isDefault: false }, async function () { + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok( + !alwaysCheck.disabled, + "'Always Check' is enabled after default browser updated" + ); + }); + gBrowser.removeCurrentTab(); + + // Testcase with Firefox set as the default browser + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + await test_with_mock_shellservice({ isDefault: true }, async function () { + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok( + alwaysCheck.disabled, + "'Always Check' is still disabled after default browser updated" + ); + }); + gBrowser.removeCurrentTab(); +}); + +registerCleanupFunction(function () { + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + Services.prefs.setBoolPref( + "browser.shell.checkDefaultBrowser", + CHECK_DEFAULT_INITIAL + ); +}); + +async function test_with_mock_shellservice(options, testFn) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [options], + async function (contentOptions) { + let doc = content.document; + let win = doc.defaultView; + win.oldShellService = win.getShellService(); + let mockShellService = { + _isDefault: false, + isDefaultBrowser() { + return this._isDefault; + }, + setDefaultBrowser() { + this._isDefault = true; + }, + }; + win.getShellService = function () { + return mockShellService; + }; + mockShellService._isDefault = contentOptions.isDefault; + win.gMainPane.updateSetDefaultBrowser(); + } + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], testFn); + + Services.prefs.setBoolPref( + "browser.shell.checkDefaultBrowser", + CHECK_DEFAULT_INITIAL + ); +} diff --git a/browser/components/preferences/tests/browser_engines.js b/browser/components/preferences/tests/browser_engines.js new file mode 100644 index 0000000000..aa681e7039 --- /dev/null +++ b/browser/components/preferences/tests/browser_engines.js @@ -0,0 +1,141 @@ +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +SearchTestUtils.init(this); + +function getCellText(tree, i, cellName) { + return tree.view.getCellText(i, tree.columns.getNamedColumn(cellName)); +} + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + keyword: ["testing", "customkeyword"], + search_url: "https://example.com/engine1", + search_url_get_params: "search={searchTerms}", + }); +}); + +add_task(async function test_engine_list() { + let prefs = await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneSearch", "Search pane is selected by default"); + let doc = gBrowser.contentDocument; + + let tree = doc.querySelector("#engineList"); + ok( + !tree.hidden, + "The search engine list should be visible when Search is requested" + ); + + // Check for default search engines to be displayed in the engineList + let defaultEngines = await Services.search.getAppProvidedEngines(); + for (let i = 0; i < defaultEngines.length; i++) { + let engine = defaultEngines[i]; + is( + getCellText(tree, i, "engineName"), + engine.name, + "Default search engine " + engine.name + " displayed correctly" + ); + } + + let customEngineIndex = defaultEngines.length; + is( + getCellText(tree, customEngineIndex, "engineKeyword"), + "testing, customkeyword", + "Show internal aliases" + ); + + // Scroll the treeview into view since mouse operations + // off screen can act confusingly. + tree.scrollIntoView(); + let rect = tree.getCoordsForCellItem( + customEngineIndex, + tree.columns.getNamedColumn("engineKeyword"), + "text" + ); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + let win = tree.ownerGlobal; + + let promise = BrowserTestUtils.waitForEvent(tree, "dblclick"); + EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win); + EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win); + await promise; + + EventUtils.sendString("newkeyword"); + EventUtils.sendKey("RETURN"); + + await TestUtils.waitForCondition(() => { + return ( + getCellText(tree, customEngineIndex, "engineKeyword") === + "newkeyword, testing, customkeyword" + ); + }); + + // Avoid duplicated keywords + tree.view.setCellText( + 0, + tree.columns.getNamedColumn("engineKeyword"), + "keyword" + ); + tree.view.setCellText( + 1, + tree.columns.getNamedColumn("engineKeyword"), + "keyword" + ); + isnot( + getCellText(tree, 1, "engineKeyword"), + "keyword", + "Do not allow duplicated keywords" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_remove_button_disabled_state() { + let prefs = await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneSearch", "Search pane is selected by default"); + let doc = gBrowser.contentDocument; + + let tree = doc.querySelector("#engineList"); + ok( + !tree.hidden, + "The search engine list should be visible when Search is requested" + ); + + let defaultEngines = await Services.search.getAppProvidedEngines(); + for (let i = 0; i < defaultEngines.length; i++) { + let engine = defaultEngines[i]; + + let isDefaultSearchEngine = + engine.name == Services.search.defaultEngine.name || + engine.name == Services.search.defaultPrivateEngine.name; + + tree.scrollIntoView(); + let rect = tree.getCoordsForCellItem( + i, + tree.columns.getNamedColumn("engineName"), + "text" + ); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + let win = tree.ownerGlobal; + + let promise = BrowserTestUtils.waitForEvent(tree, "click"); + EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win); + await promise; + + let removeButton = doc.querySelector("#removeEngineButton"); + is( + removeButton.disabled, + isDefaultSearchEngine, + "Remove button is in correct disable state" + ); + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_etp_exceptions_dialog.js b/browser/components/preferences/tests/browser_etp_exceptions_dialog.js new file mode 100644 index 0000000000..349223995c --- /dev/null +++ b/browser/components/preferences/tests/browser_etp_exceptions_dialog.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +const TRACKING_URL = "https://example.com"; + +async function openETPExceptionsDialog(doc) { + let exceptionsButton = doc.getElementById("trackingProtectionExceptions"); + ok(exceptionsButton, "trackingProtectionExceptions button found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + exceptionsButton.click(); + let dialog = await dialogPromise; + return dialog; +} + +async function addETPPermission(doc) { + let dialog = await openETPExceptionsDialog(doc); + let url = dialog.document.getElementById("url"); + let buttonDisableETP = dialog.document.getElementById("btnDisableETP"); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let currentPermissions = permissionsBox.itemCount; + + url.value = TRACKING_URL; + url.dispatchEvent(new Event("input", { bubbles: true })); + is( + buttonDisableETP.hasAttribute("disabled"), + false, + "Disable ETP button is selectable after url is entered" + ); + buttonDisableETP.click(); + + // Website is listed + is( + permissionsBox.itemCount, + currentPermissions + 1, + "Website added in url should be in the list" + ); + let saveButton = dialog.document.querySelector("dialog").getButton("accept"); + saveButton.click(); + BrowserTestUtils.waitForEvent(dialog, "unload"); +} + +async function removeETPPermission(doc) { + let dialog = await openETPExceptionsDialog(doc); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let elements = permissionsBox.getElementsByAttribute("origin", TRACKING_URL); + // Website is listed + ok(permissionsBox.itemCount, "List is not empty"); + permissionsBox.selectItem(elements[0]); + let removePermissionButton = + dialog.document.getElementById("removePermission"); + is( + removePermissionButton.hasAttribute("disabled"), + false, + "The button should be clickable to remove selected item" + ); + removePermissionButton.click(); + + let saveButton = dialog.document.querySelector("dialog").getButton("accept"); + saveButton.click(); + BrowserTestUtils.waitForEvent(dialog, "unload"); +} + +async function checkShieldIcon(shieldIcon) { + // Open the website and check that the tracking protection icon is enabled/disabled + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TRACKING_URL); + let icon = document.getElementById("tracking-protection-icon"); + is( + gBrowser.ownerGlobal + .getComputedStyle(icon) + .getPropertyValue("list-style-image"), + shieldIcon, + `The tracking protection icon shows the icon ${shieldIcon}` + ); + BrowserTestUtils.removeTab(tab); +} + +// test adds and removes an ETP permission via the about:preferences#privacy and checks if the ProtectionsUI shield icon resembles the state +add_task(async function ETPPermissionSyncedFromPrivacyPane() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + await addETPPermission(doc); + await checkShieldIcon( + `url("chrome://browser/skin/tracking-protection-disabled.svg")` + ); + await removeETPPermission(doc); + await checkShieldIcon(`url("chrome://browser/skin/tracking-protection.svg")`); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_experimental_features.js b/browser/components/preferences/tests/browser_experimental_features.js new file mode 100644 index 0000000000..cecbb60893 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testPrefRequired() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", false]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let experimentalCategory = doc.getElementById("category-experimental"); + ok(experimentalCategory, "The category exists"); + ok(experimentalCategory.hidden, "The category is hidden"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testCanOpenWithPref() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let experimentalCategory = doc.getElementById("category-experimental"); + ok(experimentalCategory, "The category exists"); + ok(!experimentalCategory.hidden, "The category is not hidden"); + + let categoryHeader = await TestUtils.waitForCondition( + () => doc.getElementById("firefoxExperimentalCategory"), + "Waiting for experimental features category to get initialized" + ); + ok( + categoryHeader.hidden, + "The category header should be hidden when Home is selected" + ); + + EventUtils.synthesizeMouseAtCenter(experimentalCategory, {}, doc.ownerGlobal); + await TestUtils.waitForCondition( + () => !categoryHeader.hidden, + "Waiting until category is visible" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testSearchFindsExperiments() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let experimentalCategory = doc.getElementById("category-experimental"); + ok(experimentalCategory, "The category exists"); + ok(!experimentalCategory.hidden, "The category is not hidden"); + + await TestUtils.waitForCondition( + () => doc.getElementById("firefoxExperimentalCategory"), + "Waiting for experimental features category to get initialized" + ); + await evaluateSearchResults( + "advanced configuration", + ["pane-experimental-featureGates"], + /* include experiments */ true + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_experimental_features_filter.js b/browser/components/preferences/tests/browser_experimental_features_filter.js new file mode 100644 index 0000000000..6bd66db555 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features_filter.js @@ -0,0 +1,183 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test verifies that searching filters the features to just that subset that +// contains the search terms. +add_task(async function testFilterFeatures() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + // Add a number of test features. + const server = new DefinitionServer(); + let definitions = [ + { + id: "test-featureA", + preference: "test.featureA", + title: "Experimental Feature 1", + description: "This is a fun experimental feature you can enable", + result: true, + }, + { + id: "test-featureB", + preference: "test.featureB", + title: "Experimental Thing 2", + description: "This is a very boring experimental tool", + result: false, + }, + { + id: "test-featureC", + preference: "test.featureC", + title: "Experimental Thing 3", + description: "This is a fun experimental feature for you can enable", + result: true, + }, + { + id: "test-featureD", + preference: "test.featureD", + title: "Experimental Thing 4", + description: "This is a not a checkbox that you should be enabling", + result: false, + }, + ]; + for (let { id, preference } of definitions) { + server.addDefinition({ id, preference, isPublic: true }); + } + + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById(definitions[definitions.length - 1].id), + "wait for the first public feature to get added to the DOM" + ); + + // Manually modify the labels of the features that were just added, so that the test + // can rely on consistent search terms. + for (let definition of definitions) { + let mainItem = doc.getElementById(definition.id); + mainItem.label = definition.title; + mainItem.removeAttribute("data-l10n-id"); + let descItem = doc.getElementById(definition.id + "-description"); + descItem.textContent = definition.description; + descItem.removeAttribute("data-l10n-id"); + } + + // First, check that all of the items are visible by default. + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + `${definition.id} should be initially visible` + ); + } + + // After searching, only a subset should be visible. + await enterSearch(doc, "feature"); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + definition.result, + `${definition.id} should be ${ + definition.result ? "visible" : "hidden" + } after first search` + ); + info("Text for item was: " + doc.getElementById(definition.id).textContent); + } + + // Further restrict the search to only a single item. + await enterSearch(doc, " you"); + + let shouldBeVisible = true; + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + shouldBeVisible, + `${definition.id} should be ${ + shouldBeVisible ? "visible" : "hidden" + } after further search` + ); + shouldBeVisible = false; + } + + // Reset the search entirely. + let searchInput = doc.getElementById("searchInput"); + searchInput.value = ""; + searchInput.doCommand(); + + // Clearing the search will go to the general pane so switch back to the experimental pane. + EventUtils.synthesizeMouseAtCenter( + doc.getElementById("category-experimental"), + {}, + gBrowser.contentWindow + ); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + `${definition.id} should be visible after search cleared` + ); + } + + // Simulate entering a search and then clicking one of the category labels. The search + // should reset each time. + for (let category of ["category-search", "category-experimental"]) { + await enterSearch(doc, "feature"); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + definition.result, + `${definition.id} should be ${ + definition.result ? "visible" : "hidden" + } after next search` + ); + } + + EventUtils.synthesizeMouseAtCenter( + doc.getElementById(category), + {}, + gBrowser.contentWindow + ); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + `${definition.id} should be visible after category change to ${category}` + ); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +function checkVisibility(element, expected, desc) { + return expected + ? is_element_visible(element, desc) + : is_element_hidden(element, desc); +} + +function enterSearch(doc, query) { + let searchInput = doc.getElementById("searchInput"); + searchInput.focus(); + + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + + EventUtils.sendString(query); + + return searchCompletedPromise; +} diff --git a/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js b/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js new file mode 100644 index 0000000000..e1e2adced9 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testNonPublicFeaturesShouldntGetDisplayed() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + const server = new DefinitionServer(); + let definitions = [ + { id: "test-featureA", isPublic: true, preference: "test.feature.a" }, + { id: "test-featureB", isPublic: false, preference: "test.feature.b" }, + { id: "test-featureC", isPublic: true, preference: "test.feature.c" }, + ]; + for (let { id, isPublic, preference } of definitions) { + server.addDefinition({ id, isPublic, preference }); + } + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById(definitions.find(d => d.isPublic).id), + "wait for the first public feature to get added to the DOM" + ); + + for (let definition of definitions) { + is( + !!doc.getElementById(definition.id), + definition.isPublic, + "feature should only be in DOM if it's public: " + definition.id + ); + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testNonPublicFeaturesShouldntGetDisplayed() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.experimental", true], + ["browser.preferences.experimental.hidden", false], + ], + }); + + const server = new DefinitionServer(); + server.addDefinition({ + id: "test-hidden", + isPublic: false, + preference: "test.feature.hidden", + }); + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById("category-experimental").hidden, + "Wait for Experimental Features section to get hidden" + ); + + ok( + doc.getElementById("category-experimental").hidden, + "Experimental Features section should be hidden when all features are hidden" + ); + ok( + !doc.getElementById("firefoxExperimentalCategory"), + "Experimental Features header should not exist when all features are hidden" + ); + is( + doc.querySelector(".category[selected]").id, + "category-general", + "When the experimental features section is hidden, navigating to #experimental should redirect to #general" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_experimental_features_resetall.js b/browser/components/preferences/tests/browser_experimental_features_resetall.js new file mode 100644 index 0000000000..636374c057 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features_resetall.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// It doesn't matter what two preferences are used here, as long as the first is a built-in +// one that defaults to false and the second defaults to true. +const KNOWN_PREF_1 = "browser.display.use_system_colors"; +const KNOWN_PREF_2 = "browser.underline_anchors"; + +// This test verifies that pressing the reset all button for experimental features +// resets all of the checkboxes to their default state. +add_task(async function testResetAll() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.experimental", true], + ["test.featureA", false], + ["test.featureB", true], + [KNOWN_PREF_1, false], + [KNOWN_PREF_2, true], + ], + }); + + // Add a number of test features. + const server = new DefinitionServer(); + let definitions = [ + { + id: "test-featureA", + preference: "test.featureA", + defaultValue: false, + }, + { + id: "test-featureB", + preference: "test.featureB", + defaultValue: true, + }, + { + id: "test-featureC", + preference: KNOWN_PREF_1, + defaultValue: false, + }, + { + id: "test-featureD", + preference: KNOWN_PREF_2, + defaultValue: true, + }, + ]; + for (let { id, preference, defaultValue } of definitions) { + server.addDefinition({ id, preference, defaultValue, isPublic: true }); + } + + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById(definitions[definitions.length - 1].id), + "wait for the first public feature to get added to the DOM" + ); + + // Check the initial state of each feature. + ok(!Services.prefs.getBoolPref("test.featureA"), "initial state A"); + ok(Services.prefs.getBoolPref("test.featureB"), "initial state B"); + ok(!Services.prefs.getBoolPref(KNOWN_PREF_1), "initial state C"); + ok(Services.prefs.getBoolPref(KNOWN_PREF_2), "initial state D"); + + // Modify the state of some of the features. + doc.getElementById("test-featureC").click(); + doc.getElementById("test-featureD").click(); + ok(!Services.prefs.getBoolPref("test.featureA"), "modified state A"); + ok(Services.prefs.getBoolPref("test.featureB"), "modified state B"); + ok(Services.prefs.getBoolPref(KNOWN_PREF_1), "modified state C"); + ok(!Services.prefs.getBoolPref(KNOWN_PREF_2), "modified state D"); + + // State after reset. + let prefChangedPromise = new Promise(resolve => { + Services.prefs.addObserver(KNOWN_PREF_2, function observer() { + Services.prefs.removeObserver(KNOWN_PREF_2, observer); + resolve(); + }); + }); + doc.getElementById("experimentalCategory-reset").click(); + await prefChangedPromise; + + // The preferences will be reset to the default value for the feature. + ok(!Services.prefs.getBoolPref("test.featureA"), "after reset state A"); + ok(Services.prefs.getBoolPref("test.featureB"), "after reset state B"); + ok(!Services.prefs.getBoolPref(KNOWN_PREF_1), "after reset state C"); + ok(Services.prefs.getBoolPref(KNOWN_PREF_2), "after reset state D"); + ok( + !doc.getElementById("test-featureA").checked, + "after reset checkbox state A" + ); + ok( + doc.getElementById("test-featureB").checked, + "after reset checkbox state B" + ); + ok( + !doc.getElementById("test-featureC").checked, + "after reset checkbox state C" + ); + ok( + doc.getElementById("test-featureD").checked, + "after reset checkbox state D" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_extension_controlled.js b/browser/components/preferences/tests/browser_extension_controlled.js new file mode 100644 index 0000000000..6cec9ba93a --- /dev/null +++ b/browser/components/preferences/tests/browser_extension_controlled.js @@ -0,0 +1,1447 @@ +/* eslint-env webextensions */ + +const PROXY_PREF = "network.proxy.type"; +const HOMEPAGE_URL_PREF = "browser.startup.homepage"; +const HOMEPAGE_OVERRIDE_KEY = "homepage_override"; +const URL_OVERRIDES_TYPE = "url_overrides"; +const NEW_TAB_KEY = "newTabURL"; +const PREF_SETTING_TYPE = "prefs"; + +ChromeUtils.defineESModuleGetters(this, { + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", +}); + +ChromeUtils.defineModuleGetter( + this, + "AboutNewTab", + "resource:///modules/AboutNewTab.jsm" +); + +XPCOMUtils.defineLazyPreferenceGetter(this, "proxyType", PROXY_PREF); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +AddonTestUtils.initMochitest(this); + +const { ExtensionPreferencesManager } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPreferencesManager.sys.mjs" +); + +const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +const CHROME_URL_ROOT = TEST_DIR + "/"; +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml"; +let sitePermissionsDialog; + +function getSupportsFile(path) { + let cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + let uri = Services.io.newURI(CHROME_URL_ROOT + path); + let fileurl = cr.convertChromeURL(uri); + return fileurl.QueryInterface(Ci.nsIFileURL); +} + +function waitForMessageChange( + element, + cb, + opts = { attributes: true, attributeFilter: ["hidden"] } +) { + return waitForMutation(element, opts, cb); +} + +function getElement(id, doc = gBrowser.contentDocument) { + return doc.getElementById(id); +} + +function waitForMessageHidden(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => target.hidden + ); +} + +function waitForMessageShown(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => !target.hidden + ); +} + +function waitForEnableMessage(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => target.classList.contains("extension-controlled-disabled"), + { attributeFilter: ["class"], attributes: true } + ); +} + +function waitForMessageContent(messageId, l10nId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => doc.l10n.getAttributes(target).id === l10nId, + { childList: true } + ); +} + +async function openNotificationsPermissionDialog() { + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let settingsButton = doc.getElementById("notificationSettingsButton"); + settingsButton.click(); + }); + + sitePermissionsDialog = await dialogOpened; + await sitePermissionsDialog.document.mozSubdialogReady; +} + +async function disableExtensionViaClick(labelId, disableButtonId, doc) { + let controlledLabel = doc.getElementById(labelId); + + let enableMessageShown = waitForEnableMessage(labelId, doc); + doc.getElementById(disableButtonId).click(); + await enableMessageShown; + + let controlledDescription = controlledLabel.querySelector("description"); + is( + doc.l10n.getAttributes(controlledDescription.querySelector("label")).id, + "extension-controlled-enable", + "The user is notified of how to enable the extension again." + ); + + // The user can dismiss the enable instructions. + let hidden = waitForMessageHidden(labelId, doc); + controlledLabel.querySelector("image:last-of-type").click(); + await hidden; +} + +async function reEnableExtension(addon, labelId) { + let controlledMessageShown = waitForMessageShown(labelId); + await addon.enable(); + await controlledMessageShown; +} + +add_task(async function testExtensionControlledHomepage() { + const ADDON_ID = "@set_homepage"; + const SECOND_ADDON_ID = "@second_set_homepage"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let homepagePref = () => Services.prefs.getCharPref(HOMEPAGE_URL_PREF); + let originalHomepagePref = homepagePref(); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + let doc = gBrowser.contentDocument; + let homeModeEl = doc.getElementById("homeMode"); + let customSettingsSection = doc.getElementById("customSettings"); + + is(homeModeEl.itemCount, 3, "The menu list starts with 3 options"); + + let promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount === 4, + "wait for the addon option to be added as an option in the menu list" + ); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_homepage", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "/home.html" }, + }, + }); + await extension.startup(); + await promise; + + // The homepage is set to the default and the custom settings section is hidden + is(homeModeEl.disabled, false, "The homepage menulist is enabled"); + is( + customSettingsSection.hidden, + true, + "The custom settings element is hidden" + ); + + let addon = await AddonManager.getAddonByID(ADDON_ID); + is( + homeModeEl.value, + addon.id, + "the home select menu's value is set to the addon" + ); + + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + // Set the Menu to the default value + homeModeEl.value = "0"; + homeModeEl.dispatchEvent(new Event("command")); + await promise; + is(homepagePref(), originalHomepagePref, "homepage is set back to default"); + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable." + ); + let setting = await ExtensionPreferencesManager.getSetting( + HOMEPAGE_OVERRIDE_KEY + ); + ok(!setting.value, "the setting is not set."); + + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + // Set the menu to the addon value + homeModeEl.value = ADDON_ID; + homeModeEl.dispatchEvent(new Event("command")); + await promise; + ok( + homepagePref().startsWith("moz-extension") && + homepagePref().endsWith("home.html"), + "Home url should be provided by the extension." + ); + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("home.html"), + "The setting value is the same as the extension." + ); + + // Add a second extension, ensure it is added to the menulist and selected. + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added as an option in the menu list" + ); + let secondExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "second_set_homepage", + browser_specific_settings: { + gecko: { + id: SECOND_ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "/home2.html" }, + }, + }); + await secondExtension.startup(); + await promise; + + let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID); + is(homeModeEl.value, SECOND_ADDON_ID, "home menulist is set to the add-on"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("home2.html"), + "The setting value is the same as the extension." + ); + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 4, + "addon option is no longer an option in the menu list after disable, even if it was not selected" + ); + await addon.disable(); + await promise; + + // Ensure that re-enabling an addon adds it back to the menulist + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added again to the menulist when enabled" + ); + await addon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 4, + "addon option is no longer an option in the menu list after disable" + ); + await secondAddon.disable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added again to the menulist when enabled" + ); + await secondAddon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 3, + "addon options are no longer an option in the menu list after disabling all addons" + ); + await secondAddon.disable(); + await addon.disable(); + await promise; + + is(homeModeEl.value, "0", "addon option is not selected in the menu list"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controllable_by_this_extension", + "getLevelOfControl returns controllable_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok(!setting.value, "The setting value is back to default."); + + // The homepage elements are reset to their original state. + is(homepagePref(), originalHomepagePref, "homepage is set back to default"); + is(homeModeEl.disabled, false, "The homepage menulist is enabled"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await extension.unload(); + await secondExtension.unload(); +}); + +add_task(async function testPrefLockedHomepage() { + const ADDON_ID = "@set_homepage"; + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let homePagePref = "browser.startup.homepage"; + let buttonPrefs = [ + "pref.browser.homepage.disable_button.current_page", + "pref.browser.homepage.disable_button.bookmark_page", + "pref.browser.homepage.disable_button.restore_default", + ]; + let homeModeEl = doc.getElementById("homeMode"); + let homePageInput = doc.getElementById("homePageUrl"); + let prefs = Services.prefs.getDefaultBranch(null); + let mutationOpts = { attributes: true, attributeFilter: ["disabled"] }; + + // Helper functions. + let getButton = pref => + doc.querySelector(`.homepage-button[preference="${pref}"`); + let waitForAllMutations = () => + Promise.all( + buttonPrefs + .map(pref => waitForMutation(getButton(pref), mutationOpts)) + .concat([ + waitForMutation(homeModeEl, mutationOpts), + waitForMutation(homePageInput, mutationOpts), + ]) + ); + let getHomepage = () => + Services.prefs.getCharPref("browser.startup.homepage"); + + let originalHomepage = getHomepage(); + let extensionHomepage = "https://developer.mozilla.org/"; + let lockedHomepage = "http://www.yahoo.com"; + + let lockPrefs = () => { + buttonPrefs.forEach(pref => { + prefs.setBoolPref(pref, true); + prefs.lockPref(pref); + }); + // Do the homepage last since that's the only pref that triggers a UI update. + prefs.setCharPref(homePagePref, lockedHomepage); + prefs.lockPref(homePagePref); + }; + let unlockPrefs = () => { + buttonPrefs.forEach(pref => { + prefs.unlockPref(pref); + prefs.setBoolPref(pref, false); + }); + // Do the homepage last since that's the only pref that triggers a UI update. + prefs.unlockPref(homePagePref); + prefs.setCharPref(homePagePref, originalHomepage); + }; + + // Lock or unlock prefs then wait for all mutations to finish. + // Expects a bool indicating if we should lock or unlock. + let waitForLockMutations = lock => { + let mutationsDone = waitForAllMutations(); + if (lock) { + lockPrefs(); + } else { + unlockPrefs(); + } + return mutationsDone; + }; + + ok( + originalHomepage != extensionHomepage, + "The extension will change the homepage" + ); + + // Install an extension that sets the homepage to MDN. + let promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_homepage", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "https://developer.mozilla.org/" }, + }, + }); + await extension.startup(); + await promise; + + // Check that everything is still disabled, homepage didn't change. + is( + getHomepage(), + extensionHomepage, + "The reported homepage is set by the extension" + ); + is( + homePageInput.value, + extensionHomepage, + "The homepage is set by the extension" + ); + + // Lock all of the prefs, wait for the UI to update. + await waitForLockMutations(true); + + // Check that everything is now disabled. + is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref"); + is(homePageInput.value, lockedHomepage, "The homepage is set by the pref"); + is( + homePageInput.disabled, + true, + "The homepage is disabed when the pref is locked" + ); + + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + true, + `The ${pref} button is disabled when locked` + ); + }); + + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + ADDON_ID, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable, the pref is locked." + ); + + // Verify that the UI is selecting the extension's Id in the menulist + let unlockedPromise = TestUtils.waitForCondition( + () => homeModeEl.value == ADDON_ID, + "Homepage menulist value is equal to the extension ID" + ); + // Unlock the prefs, wait for the UI to update. + unlockPrefs(); + await unlockedPromise; + + is( + homeModeEl.disabled, + false, + "the home select element is not disabled when the pref is not locked" + ); + is( + homePageInput.disabled, + false, + "The homepage is enabled when the pref is unlocked" + ); + is( + getHomepage(), + extensionHomepage, + "The homepage is reset to extension page" + ); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + ADDON_ID, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension after prefs are unlocked." + ); + let setting = await ExtensionPreferencesManager.getSetting( + HOMEPAGE_OVERRIDE_KEY + ); + is( + setting.value, + extensionHomepage, + "The setting value is equal to the extensionHomepage." + ); + + // Uninstall the add-on. + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + await extension.unload(); + await promise; + + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok(!setting, "The setting is gone after the addon is uninstalled."); + + // Check that everything is now enabled again. + is( + getHomepage(), + originalHomepage, + "The reported homepage is reset to original value" + ); + is(homePageInput.value, "", "The homepage is empty"); + is( + homePageInput.disabled, + false, + "The homepage is enabled after clearing lock" + ); + is( + homeModeEl.disabled, + false, + "Homepage menulist is enabled after clearing lock" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + false, + `The ${pref} button is enabled when unlocked` + ); + }); + + // Lock the prefs without an extension. + await waitForLockMutations(true); + + // Check that everything is now disabled. + is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref"); + is(homePageInput.value, lockedHomepage, "The homepage is set by the pref"); + is( + homePageInput.disabled, + true, + "The homepage is disabed when the pref is locked" + ); + is( + homeModeEl.disabled, + true, + "Homepage menulist is disabled when pref is locked" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + true, + `The ${pref} button is disabled when locked` + ); + }); + + // Unlock the prefs without an extension. + await waitForLockMutations(false); + + // Check that everything is enabled again. + is( + getHomepage(), + originalHomepage, + "The homepage is reset to the original value" + ); + is(homePageInput.value, "", "The homepage is clear after being unlocked"); + is( + homePageInput.disabled, + false, + "The homepage is enabled after clearing lock" + ); + is( + homeModeEl.disabled, + false, + "Homepage menulist is enabled after clearing lock" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + false, + `The ${pref} button is enabled when unlocked` + ); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledNewTab() { + const ADDON_ID = "@set_newtab"; + const SECOND_ADDON_ID = "@second_set_newtab"; + const DEFAULT_NEWTAB = "about:newtab"; + const NEWTAB_CONTROLLED_PREF = "browser.newtab.extensionControlled"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let doc = gBrowser.contentDocument; + let newTabMenuList = doc.getElementById("newTabMode"); + // The new tab page is set to the default. + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + + let promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is added as an option in the menu list" + ); + // Install an extension that will set the new tab page. + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_newtab", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_url_overrides: { newtab: "/newtab.html" }, + }, + }); + await extension.startup(); + + await promise; + let addon = await AddonManager.getAddonByID(ADDON_ID); + + is(newTabMenuList.value, ADDON_ID, "New tab menulist is set to the add-on"); + + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + let setting = ExtensionSettingsStore.getSetting( + URL_OVERRIDES_TYPE, + NEW_TAB_KEY + ); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("newtab.html"), + "The url_overrides is set by this extension" + ); + + promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF); + // Set the menu to the default value + newTabMenuList.value = "0"; + newTabMenuList.dispatchEvent(new Event("command")); + await promise; + let newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false); + is(newTabPref, false, "the new tab is not controlled"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable." + ); + setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY); + ok(!setting.value, "The url_overrides is not set by this extension"); + + promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF); + // Set the menu to a the addon value + newTabMenuList.value = ADDON_ID; + newTabMenuList.dispatchEvent(new Event("command")); + await promise; + newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false); + is(newTabPref, true, "the new tab is controlled"); + + // Add a second extension, ensure it is added to the menulist and selected. + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added as an option in the menu list" + ); + let secondExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "second_set_newtab", + browser_specific_settings: { + gecko: { + id: SECOND_ADDON_ID, + }, + }, + chrome_url_overrides: { newtab: "/newtab2.html" }, + }, + }); + await secondExtension.startup(); + await promise; + let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID); + is( + newTabMenuList.value, + SECOND_ADDON_ID, + "New tab menulist is set to the add-on" + ); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("newtab2.html"), + "The url_overrides is set by the second extension" + ); + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is no longer an option in the menu list after disable, even if it was not selected" + ); + await addon.disable(); + await promise; + + // Ensure that re-enabling an addon adds it back to the menulist + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added again to the menulist when enabled" + ); + await addon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is no longer an option in the menu list after disable" + ); + await secondAddon.disable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added again to the menulist when enabled" + ); + await secondAddon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 2, + "addon options are all removed after disabling all" + ); + await addon.disable(); + await secondAddon.disable(); + await promise; + is( + AboutNewTab.newTabURL, + DEFAULT_NEWTAB, + "new tab page is set back to default" + ); + + // Cleanup the tabs and add-on. + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await extension.unload(); + await secondExtension.unload(); +}); + +add_task(async function testExtensionControlledWebNotificationsPermission() { + let manifest = { + manifest_version: 2, + name: "TestExtension", + version: "1.0", + description: "Testing WebNotificationsDisable", + browser_specific_settings: { gecko: { id: "@web_notifications_disable" } }, + permissions: ["browserSettings"], + browser_action: { + default_title: "Testing", + }, + }; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openNotificationsPermissionDialog(); + + let doc = sitePermissionsDialog.document; + let extensionControlledContent = doc.getElementById( + "browserNotificationsPermissionExtensionContent" + ); + + // Test that extension content is initially hidden. + ok( + extensionControlledContent.hidden, + "Extension content is initially hidden" + ); + + // Install an extension that will disable web notifications permission. + let messageShown = waitForMessageShown( + "browserNotificationsPermissionExtensionContent", + doc + ); + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + background() { + browser.browserSettings.webNotificationsDisabled.set({ value: true }); + browser.test.sendMessage("load-extension"); + }, + }); + await extension.startup(); + await extension.awaitMessage("load-extension"); + await messageShown; + + let controlledDesc = extensionControlledContent.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-web-notifications", + args: { + name: "TestExtension", + }, + }, + "The user is notified that an extension is controlling the web notifications permission" + ); + is( + extensionControlledContent.hidden, + false, + "The extension controlled row is not hidden" + ); + + // Disable the extension. + doc.getElementById("disableNotificationsPermissionExtension").click(); + + // Verify the user is notified how to enable the extension. + await waitForEnableMessage(extensionControlledContent.id, doc); + is( + doc.l10n.getAttributes(controlledDesc.querySelector("label")).id, + "extension-controlled-enable", + "The user is notified of how to enable the extension again" + ); + + // Verify the enable message can be dismissed. + let hidden = waitForMessageHidden(extensionControlledContent.id, doc); + let dismissButton = controlledDesc.querySelector("image:last-of-type"); + dismissButton.click(); + await hidden; + + // Verify that the extension controlled content in hidden again. + is( + extensionControlledContent.hidden, + true, + "The extension controlled row is now hidden" + ); + + await extension.unload(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledHomepageUninstalledAddon() { + async function checkHomepageEnabled() { + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + // The homepage is enabled. + let homepageInput = doc.getElementById("homePageUrl"); + is(homepageInput.disabled, false, "The homepage input is enabled"); + is(homepageInput.value, "", "The homepage input is empty"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + await ExtensionSettingsStore.initialize(); + + // Verify the setting isn't reported as controlled and the inputs are enabled. + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override"), + null, + "The homepage_override is not set" + ); + await checkHomepageEnabled(); + + // Disarm any pending writes before we modify the JSONFile directly. + await ExtensionSettingsStore._reloadFile(false); + + // Write out a bad store file. + let storeData = { + prefs: { + homepage_override: { + initialValue: "", + precedenceList: [ + { + id: "bad@mochi.test", + installDate: 1508802672, + value: "https://developer.mozilla.org", + enabled: true, + }, + ], + }, + }, + }; + let jsonFileName = "extension-settings.json"; + let storePath = PathUtils.join(PathUtils.profileDir, jsonFileName); + + await IOUtils.writeUTF8(storePath, JSON.stringify(storeData)); + + // Reload the ExtensionSettingsStore so it will read the file on disk. Don't + // finalize the current store since it will overwrite our file. + await ExtensionSettingsStore._reloadFile(false); + + // Verify that the setting is reported as set, but the homepage is still enabled + // since there is no matching installed extension. + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override").value, + "https://developer.mozilla.org", + "The homepage_override appears to be set" + ); + await checkHomepageEnabled(); + + // Remove the bad store file that we used. + await IOUtils.remove(storePath); + + // Reload the ExtensionSettingsStore again so it clears the data we added. + // Don't finalize the current store since it will write out the bad data. + await ExtensionSettingsStore._reloadFile(false); + + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override"), + null, + "The ExtensionSettingsStore is left empty." + ); +}); + +add_task(async function testExtensionControlledTrackingProtection() { + const TP_PREF = "privacy.trackingprotection.enabled"; + const TP_DEFAULT = false; + const EXTENSION_ID = "@set_tp"; + const CONTROLLED_LABEL_ID = + "contentBlockingTrackingProtectionExtensionContentLabel"; + const CONTROLLED_BUTTON_ID = + "contentBlockingDisableTrackingProtectionExtension"; + + let tpEnabledPref = () => Services.prefs.getBoolPref(TP_PREF); + + await SpecialPowers.pushPrefEnv({ set: [[TP_PREF, TP_DEFAULT]] }); + + function background() { + browser.privacy.websites.trackingProtectionMode.set({ value: "always" }); + } + + function verifyState(isControlled) { + is(tpEnabledPref(), isControlled, "TP pref is set to the expected value."); + + let controlledLabel = doc.getElementById(CONTROLLED_LABEL_ID); + let controlledButton = doc.getElementById(CONTROLLED_BUTTON_ID); + + is( + controlledLabel.hidden, + !isControlled, + "The extension controlled row's visibility is as expected." + ); + is( + controlledButton.hidden, + !isControlled, + "The disable extension button's visibility is as expected." + ); + if (isControlled) { + let controlledDesc = controlledLabel.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-websites-content-blocking-all-trackers", + args: { + name: "set_tp", + }, + }, + "The user is notified that an extension is controlling TP." + ); + } + + is( + doc.getElementById("trackingProtectionMenu").disabled, + isControlled, + "TP control is enabled." + ); + } + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + is( + gBrowser.currentURI.spec, + "about:preferences#privacy", + "#privacy should be in the URI for about:preferences" + ); + + verifyState(false); + + // Install an extension that sets Tracking Protection. + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "set_tp", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["privacy"], + }, + background, + }); + + let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID); + await extension.startup(); + await messageShown; + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + verifyState(true); + + await disableExtensionViaClick( + CONTROLLED_LABEL_ID, + CONTROLLED_BUTTON_ID, + doc + ); + + verifyState(false); + + // Enable the extension so we get the UNINSTALL event, which is needed by + // ExtensionPreferencesManager to clean up properly. + // TODO: BUG 1408226 + await reEnableExtension(addon, CONTROLLED_LABEL_ID); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledPasswordManager() { + const PASSWORD_MANAGER_ENABLED_PREF = "signon.rememberSignons"; + const PASSWORD_MANAGER_ENABLED_DEFAULT = true; + const CONTROLLED_BUTTON_ID = "disablePasswordManagerExtension"; + const CONTROLLED_LABEL_ID = "passwordManagerExtensionContent"; + const EXTENSION_ID = "@remember_signons"; + let manifest = { + manifest_version: 2, + name: "testPasswordManagerExtension", + version: "1.0", + description: "Testing rememberSignons", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["privacy"], + browser_action: { + default_title: "Testing rememberSignons", + }, + }; + + let passwordManagerEnabledPref = () => + Services.prefs.getBoolPref(PASSWORD_MANAGER_ENABLED_PREF); + + await SpecialPowers.pushPrefEnv({ + set: [[PASSWORD_MANAGER_ENABLED_PREF, PASSWORD_MANAGER_ENABLED_DEFAULT]], + }); + is( + passwordManagerEnabledPref(), + true, + "Password manager is enabled by default." + ); + + function verifyState(isControlled) { + is( + passwordManagerEnabledPref(), + !isControlled, + "Password manager pref is set to the expected value." + ); + let controlledLabel = + gBrowser.contentDocument.getElementById(CONTROLLED_LABEL_ID); + let controlledButton = + gBrowser.contentDocument.getElementById(CONTROLLED_BUTTON_ID); + is( + controlledLabel.hidden, + !isControlled, + "The extension's controlled row visibility is as expected." + ); + is( + controlledButton.hidden, + !isControlled, + "The extension's controlled button visibility is as expected." + ); + if (isControlled) { + let controlledDesc = controlledLabel.querySelector("description"); + Assert.deepEqual( + gBrowser.contentDocument.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-password-saving", + args: { + name: "testPasswordManagerExtension", + }, + }, + "The user is notified that an extension is controlling the remember signons pref." + ); + } + } + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + info("Verify that no extension is controlling the password manager pref."); + verifyState(false); + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + background() { + browser.privacy.services.passwordSavingEnabled.set({ value: false }); + }, + }); + let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID); + await extension.startup(); + await messageShown; + + info( + "Verify that the test extension is controlling the password manager pref." + ); + verifyState(true); + + info("Verify that the extension shows as controlled when loaded again."); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + verifyState(true); + + await disableExtensionViaClick( + CONTROLLED_LABEL_ID, + CONTROLLED_BUTTON_ID, + gBrowser.contentDocument + ); + + info( + "Verify that disabling the test extension removes the lock on the password manager pref." + ); + verifyState(false); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledProxyConfig() { + const proxySvc = Ci.nsIProtocolProxyService; + const PROXY_DEFAULT = proxySvc.PROXYCONFIG_SYSTEM; + const EXTENSION_ID = "@set_proxy"; + const CONTROLLED_SECTION_ID = "proxyExtensionContent"; + const CONTROLLED_BUTTON_ID = "disableProxyExtension"; + const CONNECTION_SETTINGS_DESC_ID = "connectionSettingsDescription"; + const PANEL_URL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + + await SpecialPowers.pushPrefEnv({ set: [[PROXY_PREF, PROXY_DEFAULT]] }); + + function background() { + browser.proxy.settings.set({ value: { proxyType: "none" } }); + } + + function expectedConnectionSettingsMessage(doc, isControlled) { + return isControlled + ? "extension-controlling-proxy-config" + : "network-proxy-connection-description"; + } + + function connectionSettingsMessagePromise(doc, isControlled) { + return waitForMessageContent( + CONNECTION_SETTINGS_DESC_ID, + expectedConnectionSettingsMessage(doc, isControlled), + doc + ); + } + + function verifyProxyState(doc, isControlled) { + let isPanel = doc.getElementById(CONTROLLED_BUTTON_ID); + is( + proxyType === proxySvc.PROXYCONFIG_DIRECT, + isControlled, + "Proxy pref is set to the expected value." + ); + + if (isPanel) { + let controlledSection = doc.getElementById(CONTROLLED_SECTION_ID); + + is( + controlledSection.hidden, + !isControlled, + "The extension controlled row's visibility is as expected." + ); + if (isPanel) { + is( + doc.getElementById(CONTROLLED_BUTTON_ID).hidden, + !isControlled, + "The disable extension button's visibility is as expected." + ); + } + if (isControlled) { + let controlledDesc = controlledSection.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-proxy-config", + args: { + name: "set_proxy", + }, + }, + "The user is notified that an extension is controlling proxy settings." + ); + } + function getProxyControls() { + let controlGroup = doc.getElementById("networkProxyType"); + let manualControlContainer = controlGroup.querySelector("#proxy-grid"); + return { + manualControls: [ + ...manualControlContainer.querySelectorAll( + "label[data-l10n-id]:not([control=networkProxyNone])" + ), + ...manualControlContainer.querySelectorAll("input"), + ...manualControlContainer.querySelectorAll("checkbox"), + ...doc.querySelectorAll("#networkProxySOCKSVersion > radio"), + ], + pacControls: [doc.getElementById("networkProxyAutoconfigURL")], + otherControls: [ + doc.querySelector("label[control=networkProxyNone]"), + doc.getElementById("networkProxyNone"), + ...controlGroup.querySelectorAll(":scope > radio"), + ...doc.querySelectorAll("#ConnectionsDialogPane > checkbox"), + ], + }; + } + let controlState = isControlled ? "disabled" : "enabled"; + let controls = getProxyControls(); + for (let element of controls.manualControls) { + let disabled = + isControlled || proxyType !== proxySvc.PROXYCONFIG_MANUAL; + is( + element.disabled, + disabled, + `Manual proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + for (let element of controls.pacControls) { + let disabled = isControlled || proxyType !== proxySvc.PROXYCONFIG_PAC; + is( + element.disabled, + disabled, + `PAC proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + for (let element of controls.otherControls) { + is( + element.disabled, + isControlled, + `Other proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + } else { + let elem = doc.getElementById(CONNECTION_SETTINGS_DESC_ID); + is( + doc.l10n.getAttributes(elem).id, + expectedConnectionSettingsMessage(doc, isControlled), + "The connection settings description is as expected." + ); + } + } + + async function reEnableProxyExtension(addon) { + let messageChanged = connectionSettingsMessagePromise(mainDoc, true); + await addon.enable(); + await messageChanged; + } + + async function openProxyPanel() { + let panel = await openAndLoadSubDialog(PANEL_URL); + let closingPromise = BrowserTestUtils.waitForEvent( + panel.document.getElementById("ConnectionsDialog"), + "dialogclosing" + ); + ok(panel, "Proxy panel opened."); + return { panel, closingPromise }; + } + + async function closeProxyPanel(panelObj) { + let dialog = panelObj.panel.document.getElementById("ConnectionsDialog"); + dialog.cancelDialog(); + let panelClosingEvent = await panelObj.closingPromise; + ok(panelClosingEvent, "Proxy panel closed."); + } + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let mainDoc = gBrowser.contentDocument; + + is( + gBrowser.currentURI.spec, + "about:preferences#general", + "#general should be in the URI for about:preferences" + ); + + verifyProxyState(mainDoc, false); + + // Open the connections panel. + let panelObj = await openProxyPanel(); + let panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + + verifyProxyState(mainDoc, false); + + // Install an extension that controls proxy settings. The extension needs + // incognitoOverride because controlling the proxy.settings requires private + // browsing access. + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + useAddonManager: "permanent", + manifest: { + name: "set_proxy", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["proxy"], + }, + background, + }); + + let messageChanged = connectionSettingsMessagePromise(mainDoc, true); + await extension.startup(); + await messageChanged; + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + verifyProxyState(mainDoc, true); + messageChanged = connectionSettingsMessagePromise(mainDoc, false); + + panelObj = await openProxyPanel(); + panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, true); + + await disableExtensionViaClick( + CONTROLLED_SECTION_ID, + CONTROLLED_BUTTON_ID, + panelDoc + ); + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + await messageChanged; + + verifyProxyState(mainDoc, false); + + await reEnableProxyExtension(addon); + + verifyProxyState(mainDoc, true); + messageChanged = connectionSettingsMessagePromise(mainDoc, false); + + panelObj = await openProxyPanel(); + panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, true); + + await disableExtensionViaClick( + CONTROLLED_SECTION_ID, + CONTROLLED_BUTTON_ID, + panelDoc + ); + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + await messageChanged; + + verifyProxyState(mainDoc, false); + + // Enable the extension so we get the UNINSTALL event, which is needed by + // ExtensionPreferencesManager to clean up properly. + // TODO: BUG 1408226 + await reEnableProxyExtension(addon); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test that the newtab menu selection is correct when loading about:preferences +add_task(async function testMenuSyncFromPrefs() { + const DEFAULT_NEWTAB = "about:newtab"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let doc = gBrowser.contentDocument; + let newTabMenuList = doc.getElementById("newTabMode"); + // The new tab page is set to the default. + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + + is(newTabMenuList.value, "0", "New tab menulist is set to the default"); + + newTabMenuList.value = "1"; + newTabMenuList.dispatchEvent(new Event("command")); + is(newTabMenuList.value, "1", "New tab menulist is set to blank"); + + gBrowser.reloadTab(gBrowser.selectedTab); + + await TestUtils.waitForCondition( + () => gBrowser.contentDocument.getElementById("newTabMode"), + "wait until element exists in new contentDoc" + ); + + is( + gBrowser.contentDocument.getElementById("newTabMode").value, + "1", + "New tab menulist is still set to blank" + ); + + // Cleanup + newTabMenuList.value = "0"; + newTabMenuList.dispatchEvent(new Event("command")); + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_filetype_dialog.js b/browser/components/preferences/tests/browser_filetype_dialog.js new file mode 100644 index 0000000000..0e5ac036c4 --- /dev/null +++ b/browser/components/preferences/tests/browser_filetype_dialog.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +SimpleTest.requestCompleteLog(); +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +let gOldMailHandlers = []; +let gDummyHandlers = []; +let gOriginalPreferredMailHandler; +let gOriginalPreferredPDFHandler; + +registerCleanupFunction(function () { + function removeDummyHandlers(handlers) { + // Remove any of the dummy handlers we created. + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + if ( + gDummyHandlers.some( + h => + h.uriTemplate == + handlers.queryElementAt(i, Ci.nsIWebHandlerApp).uriTemplate + ) + ) { + handlers.removeElementAt(i); + } + } catch (ex) { + /* ignore non-web-app handlers */ + } + } + } + // Re-add the original protocol handlers: + let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + let mailHandlers = mailHandlerInfo.possibleApplicationHandlers; + for (let h of gOldMailHandlers) { + mailHandlers.appendElement(h); + } + removeDummyHandlers(mailHandlers); + mailHandlerInfo.preferredApplicationHandler = gOriginalPreferredMailHandler; + gHandlerService.store(mailHandlerInfo); + + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally; + pdfHandlerInfo.preferredApplicationHandler = gOriginalPreferredPDFHandler; + let handlers = pdfHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + let app = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if (app.name == "Foopydoopydoo") { + handlers.removeElementAt(i); + } + } + gHandlerService.store(pdfHandlerInfo); + + gBrowser.removeCurrentTab(); +}); + +function scrubMailtoHandlers(handlerInfo) { + // Remove extant web handlers because they have icons that + // we fetch from the web, which isn't allowed in tests. + let handlers = handlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + gOldMailHandlers.push(handler); + // If we get here, this is a web handler app. Remove it: + handlers.removeElementAt(i); + } catch (ex) {} + } +} + +("use strict"); + +add_setup(async function () { + // Create our dummy handlers + let handler1 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler1.name = "Handler 1"; + handler1.uriTemplate = "https://example.com/first/%s"; + + let handler2 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler2.name = "Handler 2"; + handler2.uriTemplate = "http://example.org/second/%s"; + gDummyHandlers.push(handler1, handler2); + + function substituteWebHandlers(handlerInfo) { + // Append the dummy handlers to replace them: + let handlers = handlerInfo.possibleApplicationHandlers; + handlers.appendElement(handler1); + handlers.appendElement(handler2); + gHandlerService.store(handlerInfo); + } + // Set up our mailto handler test infrastructure. + let mailtoHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + scrubMailtoHandlers(mailtoHandlerInfo); + gOriginalPreferredMailHandler = mailtoHandlerInfo.preferredApplicationHandler; + substituteWebHandlers(mailtoHandlerInfo); + + // Now add a pdf handler: + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + // PDF doesn't have built-in web handlers, so no need to scrub. + gOriginalPreferredPDFHandler = pdfHandlerInfo.preferredApplicationHandler; + let handlers = pdfHandlerInfo.possibleApplicationHandlers; + let appHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + appHandler.name = "Foopydoopydoo"; + appHandler.executable = Services.dirsvc.get("ProfD", Ci.nsIFile); + appHandler.executable.append("dummy.exe"); + // Prefs are picky and want this to exist and be executable (bug 1626009): + if (!appHandler.executable.exists()) { + appHandler.executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o777); + } + + handlers.appendElement(appHandler); + + pdfHandlerInfo.preferredApplicationHandler = appHandler; + pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + gHandlerService.store(pdfHandlerInfo); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + info("Preferences page opened on the general pane."); + + await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList; + info("Apps list loaded."); +}); + +add_task(async function dialogShowsCorrectContent() { + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First, find the PDF item. + let pdfItem = container.querySelector("richlistitem[type='application/pdf']"); + Assert.ok(pdfItem, "pdfItem is present in handlersView."); + pdfItem.scrollIntoView({ block: "center" }); + pdfItem.closest("richlistbox").selectItem(pdfItem); + + // Open its menu + let list = pdfItem.querySelector(".actionsMenu"); + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(list, {}, win); + await popupShown; + + // Then open the dialog + const promiseDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/applicationManager.xhtml" + ); + EventUtils.synthesizeMouseAtCenter( + popup.querySelector(".manage-app-item"), + {}, + win + ); + let dialogWin = await promiseDialogLoaded; + + // Then verify that the description is correct. + let desc = dialogWin.document.getElementById("appDescription"); + let descL10n = dialogWin.document.l10n.getAttributes(desc); + is(descL10n.id, "app-manager-handle-file", "Should have right string"); + let stringBundle = Services.strings.createBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties" + ); + is( + descL10n.args.type, + stringBundle.GetStringFromName("pdfExtHandlerDescription"), + "Should have PDF string bits." + ); + + // And that there's one item in the list, with the correct name: + let appList = dialogWin.document.getElementById("appList"); + is(appList.itemCount, 1, "Should have 1 item in the list"); + is( + appList.selectedItem.querySelector("label").getAttribute("value"), + "Foopydoopydoo", + "Should have the right executable label" + ); + + dialogWin.close(); +}); diff --git a/browser/components/preferences/tests/browser_fluent.js b/browser/components/preferences/tests/browser_fluent.js new file mode 100644 index 0000000000..db2daecc4f --- /dev/null +++ b/browser/components/preferences/tests/browser_fluent.js @@ -0,0 +1,40 @@ +function whenMainPaneLoadedFinished() { + return new Promise(function (resolve, reject) { + const topic = "main-pane-loaded"; + Services.obs.addObserver(function observer(aSubject) { + Services.obs.removeObserver(observer, topic); + resolve(); + }, topic); + }); +} + +// Temporary test for an experimental new localization API. +// See bug 1402069 for details. +add_task(async function () { + // The string is used only when `browserTabsRemoteAutostart` is true + if (!Services.appinfo.browserTabsRemoteAutostart) { + ok(true, "fake test to avoid harness complaining"); + return; + } + + await Promise.all([ + openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }), + whenMainPaneLoadedFinished(), + ]); + + let doc = gBrowser.contentDocument; + await doc.l10n.ready; + + let [msg] = await doc.l10n.formatMessages([{ id: "category-general" }]); + + let elem = doc.querySelector(`#category-general`); + + Assert.deepEqual(msg, { + value: null, + attributes: [ + { name: "tooltiptext", value: elem.getAttribute("tooltiptext") }, + ], + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_homepage_default.js b/browser/components/preferences/tests/browser_homepage_default.js new file mode 100644 index 0000000000..e3fcdcec5e --- /dev/null +++ b/browser/components/preferences/tests/browser_homepage_default.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function default_homepage_test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.startup.page", 1]], + }); + let defaults = Services.prefs.getDefaultBranch(""); + // Simulate a homepage set via policy or a distribution. + defaults.setStringPref("browser.startup.homepage", "https://example.com"); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + let homeMode = doc.getElementById("homeMode"); + Assert.equal(homeMode.value, 2, "homeMode should be 2 (Custom URL)"); + + let homePageUrl = doc.getElementById("homePageUrl"); + Assert.equal( + homePageUrl.value, + "https://example.com", + "homePageUrl should be example.com" + ); + + registerCleanupFunction(async () => { + defaults.setStringPref("browser.startup.homepage", "about:home"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); diff --git a/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js b/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js new file mode 100644 index 0000000000..f54d0edaaa --- /dev/null +++ b/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js @@ -0,0 +1,33 @@ +add_task(async function testSetHomepageUseCurrent() { + is( + gBrowser.currentURI.spec, + "about:blank", + "Test starts with about:blank open" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home"); + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + let oldHomepage = HomePage.get(); + + let useCurrent = doc.getElementById("useCurrentBtn"); + useCurrent.click(); + + is(gBrowser.tabs.length, 3, "Three tabs should be open"); + await TestUtils.waitForCondition( + () => HomePage.get() == "about:blank|about:home" + ); + is( + HomePage.get(), + "about:blank|about:home", + "about:blank and about:home should be the only homepages set" + ); + + HomePage.safeSet(oldHomepage); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_homepages_use_bookmark.js b/browser/components/preferences/tests/browser_homepages_use_bookmark.js new file mode 100644 index 0000000000..572783481d --- /dev/null +++ b/browser/components/preferences/tests/browser_homepages_use_bookmark.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL1 = "http://example.com/1"; +const TEST_URL2 = "http://example.com/2"; + +add_setup(async function () { + let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage"); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + Assert.equal( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + registerCleanupFunction(async () => { + Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function testSetHomepageFromBookmark() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "TestHomepage", + url: TEST_URL1, + }); + + let doc = gBrowser.contentDocument; + // Select the custom URLs option. + doc.getElementById("homeMode").value = 2; + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml" + ); + doc.getElementById("useBookmarkBtn").click(); + + let dialog = await promiseSubDialogLoaded; + dialog.document.getElementById("bookmarks").selectItems([bm.guid]); + dialog.document + .getElementById("selectBookmarkDialog") + .getButton("accept") + .click(); + + await TestUtils.waitForCondition(() => HomePage.get() == TEST_URL1); + + Assert.equal( + HomePage.get(), + TEST_URL1, + "Should have set the homepage to the same as the bookmark." + ); +}); + +add_task(async function testSetHomepageFromTopLevelFolder() { + // Insert a second item into the menu folder + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "TestHomepage", + url: TEST_URL2, + }); + + let doc = gBrowser.contentDocument; + // Select the custom URLs option. + doc.getElementById("homeMode").value = 2; + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml" + ); + doc.getElementById("useBookmarkBtn").click(); + + let dialog = await promiseSubDialogLoaded; + dialog.document + .getElementById("bookmarks") + .selectItems([PlacesUtils.bookmarks.menuGuid]); + dialog.document + .getElementById("selectBookmarkDialog") + .getButton("accept") + .click(); + + await TestUtils.waitForCondition( + () => HomePage.get() == `${TEST_URL1}|${TEST_URL2}` + ); + + Assert.equal( + HomePage.get(), + `${TEST_URL1}|${TEST_URL2}`, + "Should have set the homepage to the same as the bookmark." + ); +}); diff --git a/browser/components/preferences/tests/browser_hometab_restore_defaults.js b/browser/components/preferences/tests/browser_hometab_restore_defaults.js new file mode 100644 index 0000000000..55a9a974b2 --- /dev/null +++ b/browser/components/preferences/tests/browser_hometab_restore_defaults.js @@ -0,0 +1,220 @@ +add_task(async function testRestoreDefaultsBtn_visible() { + const before = SpecialPowers.Services.prefs.getStringPref( + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + "" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Hide Pocket prefs so we don't trigger network requests when we reset all preferences + [ + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + JSON.stringify(Object.assign({}, JSON.parse(before), { hidden: true })), + ], + [ + "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear", + "", + ], + // Set a user pref to false to force the Restore Defaults button to be visible + ["browser.newtabpage.activity-stream.feeds.topsites", false], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#home", + false + ); + let browser = tab.linkedBrowser; + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn") !== null + ), + "Wait for the button to be added to the page" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ) !== null + ), + "Wait for the preference checkbox to load" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn") + .hidden === false + ), + "Should show the Restore Defaults btn because pref is changed" + ); + + await SpecialPowers.spawn(browser, [], () => + content.document.getElementById("restoreDefaultHomePageBtn").click() + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ).checked + ), + "Should have checked preference" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn").style + .visibility === "hidden" + ), + "Should not show the Restore Defaults btn if prefs were reset" + ); + + const topsitesPref = await SpecialPowers.Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.feeds.topsites" + ); + Assert.ok(topsitesPref, "Topsites pref should have the default value"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testRestoreDefaultsBtn_hidden() { + const before = SpecialPowers.Services.prefs.getStringPref( + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + "" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Hide Pocket pref so we don't trigger network requests when we reset all preferences + [ + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + JSON.stringify(Object.assign({}, JSON.parse(before), { hidden: true })), + ], + [ + "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear", + "", + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#home", + false + ); + let browser = tab.linkedBrowser; + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn") !== null + ), + "Wait for the button to be added to the page" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ) !== null + ), + "Wait for the preference checkbox to load" + ); + + const btnDefault = await SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn").style + .visibility + ); + Assert.equal( + btnDefault, + "hidden", + "When no prefs are changed button should not show up" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ).checked + ), + "Should have checked preference" + ); + + // Uncheck a pref + await SpecialPowers.spawn(browser, [], () => + content.document + .querySelector("[data-subcategory='topsites'] checkbox") + .click() + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + !content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ).checked + ), + "Should have unchecked preference" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn").style + .visibility === "" + ), + "Should show the Restore Defaults btn if prefs were changed" + ); + + // Reset the pref + await SpecialPowers.Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.topsites" + ); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_https_only_exceptions.js b/browser/components/preferences/tests/browser_https_only_exceptions.js new file mode 100644 index 0000000000..ad7cd34571 --- /dev/null +++ b/browser/components/preferences/tests/browser_https_only_exceptions.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * First Test + * Checks if buttons are disabled/enabled and visible/hidden correctly. + */ +add_task(async function testButtons() { + // Let's make sure HTTPS-Only Mode is off. + await setHttpsOnlyPref("off"); + + // Open the privacy-pane in about:preferences + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + // Get button-element to open the exceptions-dialog + const exceptionButton = gBrowser.contentDocument.getElementById( + "httpsOnlyExceptionButton" + ); + + is( + exceptionButton.disabled, + true, + "HTTPS-Only exception button should be disabled when HTTPS-Only Mode is disabled." + ); + + await setHttpsOnlyPref("private"); + is( + exceptionButton.disabled, + true, + "HTTPS-Only exception button should be disabled when HTTPS-Only Mode is only enabled in private browsing." + ); + + await setHttpsOnlyPref("everywhere"); + is( + exceptionButton.disabled, + false, + "HTTPS-Only exception button should be enabled when HTTPS-Only Mode enabled everywhere." + ); + + // Now that the button is clickable, we open the dialog + // to check if the correct buttons are visible + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + exceptionButton.doCommand(); + + let win = await promiseSubDialogLoaded; + + const dialogDoc = win.document; + + is( + dialogDoc.getElementById("btnBlock").hidden, + true, + "Block button should not be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnCookieSession").hidden, + true, + "Cookie specific allow button should not be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnAllow").hidden, + true, + "Allow button should not be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnHttpsOnlyOff").hidden, + false, + "HTTPS-Only off button should be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnHttpsOnlyOffTmp").hidden, + false, + "HTTPS-Only temporary off button should be visible in HTTPS-Only Dialog." + ); + + // Reset prefs and close the tab + await SpecialPowers.flushPrefEnv(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Second Test + * Checks permissions are added and removed correctly. + * + * Each test opens a new dialog, performs an action (second argument), + * then closes the dialog and checks if the changes were made (third argument). + */ +add_task(async function checkDialogFunctionality() { + // Enable HTTPS-Only Mode for every window, so the exceptions dialog is accessible. + await setHttpsOnlyPref("everywhere"); + + // Open the privacy-pane in about:preferences + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + const preferencesDoc = gBrowser.contentDocument; + + // Test if we can add permanent exceptions + await runTest( + preferencesDoc, + elements => { + assertListContents(elements, []); + + elements.url.value = "test.com"; + elements.btnAllow.doCommand(); + + assertListContents(elements, [ + ["http://test.com", elements.allowL10nId], + ["https://test.com", elements.allowL10nId], + ]); + }, + () => [ + { + type: "https-only-load-insecure", + origin: "http://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: Ci.nsIPermissionManager.EXPIRE_NEVER, + }, + { + type: "https-only-load-insecure", + origin: "https://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: Ci.nsIPermissionManager.EXPIRE_NEVER, + }, + ] + ); + + // Test if items are retained, and if temporary exceptions are added correctly + await runTest( + preferencesDoc, + elements => { + assertListContents(elements, [ + ["http://test.com", elements.allowL10nId], + ["https://test.com", elements.allowL10nId], + ]); + + elements.url.value = "1.1.1.1:8080"; + elements.btnAllowSession.doCommand(); + + assertListContents(elements, [ + ["http://test.com", elements.allowL10nId], + ["https://test.com", elements.allowL10nId], + ["http://1.1.1.1:8080", elements.allowSessionL10nId], + ["https://1.1.1.1:8080", elements.allowSessionL10nId], + ]); + }, + () => [ + { + type: "https-only-load-insecure", + origin: "http://1.1.1.1:8080", + data: "added", + capability: Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, + expireType: Ci.nsIPermissionManager.EXPIRE_SESSION, + }, + { + type: "https-only-load-insecure", + origin: "https://1.1.1.1:8080", + data: "added", + capability: Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, + expireType: Ci.nsIPermissionManager.EXPIRE_SESSION, + }, + ] + ); + + await runTest( + preferencesDoc, + elements => { + while (elements.richlistbox.itemCount) { + elements.richlistbox.selectedIndex = 0; + elements.btnRemove.doCommand(); + } + assertListContents(elements, []); + }, + elements => { + let richlistItems = elements.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "https-only-load-insecure", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Changes HTTPS-Only Mode pref + * @param {string} state "everywhere", "private", "off" + */ +async function setHttpsOnlyPref(state) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode", state === "everywhere"], + ["dom.security.https_only_mode_pbm", state === "private"], + ], + }); +} + +/** + * Opens new exceptions dialog, runs test function + * @param {HTMLElement} preferencesDoc document of about:preferences tab + * @param {function} test function to call when dialog is open + * @param {Array} observances permission changes to observe (order is important) + */ +async function runTest(preferencesDoc, test, observancesFn) { + // Click on exception-button and wait for dialog to open + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + preferencesDoc.getElementById("httpsOnlyExceptionButton").doCommand(); + + let win = await promiseSubDialogLoaded; + + // Create a bunch of references to UI-elements for the test-function + const doc = win.document; + let elements = { + richlistbox: doc.getElementById("permissionsBox"), + url: doc.getElementById("url"), + btnAllow: doc.getElementById("btnHttpsOnlyOff"), + btnAllowSession: doc.getElementById("btnHttpsOnlyOffTmp"), + btnRemove: doc.getElementById("removePermission"), + allowL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIPermissionManager.ALLOW_ACTION + ), + allowSessionL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION + ), + }; + + // Some observances need to be computed based on the current state. + const observances = observancesFn(elements); + + // Run test function + await test(elements); + + // Click on "Save changes" and wait for permission changes. + let btnApplyChanges = doc.querySelector("dialog").getButton("accept"); + let observeAllPromise = createObserveAllPromise(observances); + + btnApplyChanges.doCommand(); + await observeAllPromise; +} + +function assertListContents(elements, expected) { + is( + elements.richlistbox.itemCount, + expected.length, + "Richlistbox should match the expected amount of exceptions." + ); + + for (let i = 0; i < expected.length; i++) { + let website = expected[i][0]; + let listItem = elements.richlistbox.getElementsByAttribute( + "origin", + website + ); + is(listItem.length, 1, "Each origin should be unique"); + is( + listItem[0] + .querySelector(".website-capability-value") + .getAttribute("data-l10n-id"), + expected[i][1], + "List item capability should match expected l10n-id" + ); + } +} diff --git a/browser/components/preferences/tests/browser_https_only_section.js b/browser/components/preferences/tests/browser_https_only_section.js new file mode 100644 index 0000000000..a43448adf5 --- /dev/null +++ b/browser/components/preferences/tests/browser_https_only_section.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Bug 1671122 - Fixed bug where second click on HTTPS-Only Mode enable-checkbox disables it again. +// https://bugzilla.mozilla.org/bug/1671122 +"use strict"; + +const HTTPS_ONLY_ENABLED = "enabled"; +const HTTPS_ONLY_PBM_ONLY = "privateOnly"; +const HTTPS_ONLY_DISABLED = "disabled"; + +add_task(async function httpsOnlyRadioGroupIsWorking() { + // Make sure HTTPS-Only mode is only enabled for PBM + + registerCleanupFunction(async function () { + Services.prefs.clearUserPref("dom.security.https_only_mode"); + Services.prefs.clearUserPref("dom.security.https_only_mode_pbm"); + }); + + await SpecialPowers.setBoolPref("dom.security.https_only_mode", false); + await SpecialPowers.setBoolPref("dom.security.https_only_mode_pbm", true); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const radioGroup = doc.getElementById("httpsOnlyRadioGroup"); + const enableAllRadio = doc.getElementById("httpsOnlyRadioEnabled"); + const enablePbmRadio = doc.getElementById("httpsOnlyRadioEnabledPBM"); + const disableRadio = doc.getElementById("httpsOnlyRadioDisabled"); + + // Check if UI + check(radioGroup, HTTPS_ONLY_PBM_ONLY); + + // Check if UI updated on pref-change + await SpecialPowers.setBoolPref("dom.security.https_only_mode_pbm", false); + check(radioGroup, HTTPS_ONLY_DISABLED); + + // Check if prefs change if clicked on radio button + enableAllRadio.click(); + check(radioGroup, HTTPS_ONLY_ENABLED); + + // Check if prefs stay the same if clicked on same + // radio button again (see bug 1671122) + enableAllRadio.click(); + check(radioGroup, HTTPS_ONLY_ENABLED); + + // Check if prefs are set correctly for PBM-only mode. + enablePbmRadio.click(); + check(radioGroup, HTTPS_ONLY_PBM_ONLY); + + // Check if prefs are set correctly when disabled again. + disableRadio.click(); + check(radioGroup, HTTPS_ONLY_DISABLED); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +function check(radioGroupElement, expectedValue) { + is( + radioGroupElement.value, + expectedValue, + "Radio Group value should match expected value" + ); + is( + SpecialPowers.getBoolPref("dom.security.https_only_mode"), + expectedValue === HTTPS_ONLY_ENABLED, + "HTTPS-Only pref should match expected value." + ); + is( + SpecialPowers.getBoolPref("dom.security.https_only_mode_pbm"), + expectedValue === HTTPS_ONLY_PBM_ONLY, + "HTTPS-Only PBM pref should match expected value." + ); +} diff --git a/browser/components/preferences/tests/browser_ignore_invalid_capability.js b/browser/components/preferences/tests/browser_ignore_invalid_capability.js new file mode 100644 index 0000000000..35b2679ffc --- /dev/null +++ b/browser/components/preferences/tests/browser_ignore_invalid_capability.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function testInvalidCapabilityIgnored() { + info( + "Test to make sure that invalid combinations of type and capability are ignored \ + so the cookieExceptions management popup does not crash" + ); + PermissionTestUtils.add( + "https://mozilla.org", + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + // This is an invalid combination of type & capability and should be ignored + PermissionTestUtils.add( + "https://foobar.org", + "cookie", + Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION + ); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + doc.getElementById("cookieExceptions").doCommand(); + + let win = await promiseSubDialogLoaded; + doc = win.document; + + is( + doc.getElementById("permissionsBox").itemCount, + 1, + "We only display the permission that is valid for the type cookie" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_languages_subdialog.js b/browser/components/preferences/tests/browser_languages_subdialog.js new file mode 100644 index 0000000000..e85ce44ca3 --- /dev/null +++ b/browser/components/preferences/tests/browser_languages_subdialog.js @@ -0,0 +1,139 @@ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + const contentDocument = gBrowser.contentDocument; + let dialogOverlay = content.gSubDialog._preloadDialog._overlay; + + async function languagesSubdialogOpened() { + const promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/languages.xhtml" + ); + contentDocument.getElementById("chooseLanguage").click(); + const win = await promiseSubDialogLoaded; + dialogOverlay = content.gSubDialog._topDialog._overlay; + ok(!BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is visible."); + return win; + } + + function acceptLanguagesSubdialog(win) { + const button = win.document.querySelector("dialog").getButton("accept"); + button.doCommand(); + } + + ok(BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is invisible."); + let win = await languagesSubdialogOpened(); + ok( + win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox is hidden." + ); + acceptLanguagesSubdialog(win); + ok(BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is invisible."); + + await SpecialPowers.pushPrefEnv({ + set: [["intl.accept_languages", "en-US,en-XX,foo"]], + }); + win = await languagesSubdialogOpened(); + let activeLanguages = win.document.getElementById("activeLanguages").children; + ok( + activeLanguages[0].id == "en-us", + "The ID for 'en-US' locale code is correctly set." + ); + ok( + activeLanguages[0].firstChild.value == "English (United States) [en-us]", + "The name for known 'en-US' locale code is correctly resolved." + ); + ok( + activeLanguages[1].id == "en-xx", + "The ID for 'en-XX' locale code is correctly set." + ); + ok( + activeLanguages[1].firstChild.value == "English [en-xx]", + "The name for unknown 'en-XX' locale code is resolved using 'en'." + ); + ok( + activeLanguages[2].firstChild.value == " [foo]", + "The name for unknown 'foo' locale code is empty." + ); + acceptLanguagesSubdialog(win); + await SpecialPowers.popPrefEnv(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["privacy.spoof_english", 0], + ], + }); + + win = await languagesSubdialogOpened(); + ok( + !win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox isn't hidden." + ); + ok( + !win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox isn't checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 0, + "The privacy.spoof_english pref is set to 0." + ); + + win.document.getElementById("spoofEnglish").checked = true; + win.document.getElementById("spoofEnglish").doCommand(); + ok( + win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox is checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 2, + "The privacy.spoof_english pref is set to 2." + ); + acceptLanguagesSubdialog(win); + + win = await languagesSubdialogOpened(); + ok( + !win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox isn't hidden." + ); + ok( + win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox is checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 2, + "The privacy.spoof_english pref is set to 2." + ); + + win.document.getElementById("spoofEnglish").checked = false; + win.document.getElementById("spoofEnglish").doCommand(); + ok( + !win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox isn't checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 1, + "The privacy.spoof_english pref is set to 1." + ); + acceptLanguagesSubdialog(win); + + win = await languagesSubdialogOpened(); + ok( + !win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox isn't hidden." + ); + ok( + !win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox isn't checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 1, + "The privacy.spoof_english pref is set to 1." + ); + acceptLanguagesSubdialog(win); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_layersacceleration.js b/browser/components/preferences/tests/browser_layersacceleration.js new file mode 100644 index 0000000000..982a32c94d --- /dev/null +++ b/browser/components/preferences/tests/browser_layersacceleration.js @@ -0,0 +1,36 @@ +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["gfx.direct2d.disabled", false], + ["layers.acceleration.disabled", false], + ], + }); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let checkbox = doc.querySelector("#allowHWAccel"); + is( + !checkbox.checked, + Services.prefs.getBoolPref("layers.acceleration.disabled"), + "checkbox should represent inverted pref value before clicking on checkbox" + ); + + checkbox.click(); + + is( + !checkbox.checked, + Services.prefs.getBoolPref("layers.acceleration.disabled"), + "checkbox should represent inverted pref value after clicking on checkbox" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_localSearchShortcuts.js b/browser/components/preferences/tests/browser_localSearchShortcuts.js new file mode 100644 index 0000000000..0b8e170cc1 --- /dev/null +++ b/browser/components/preferences/tests/browser_localSearchShortcuts.js @@ -0,0 +1,309 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Checks the local shortcut rows in the engines list of the search pane. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +let gTree; + +add_setup(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + registerCleanupFunction(() => { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); + + Assert.equal( + prefs.selectedPane, + "paneSearch", + "Sanity check: Search pane is selected by default" + ); + + gTree = gBrowser.contentDocument.querySelector("#engineList"); + gTree.scrollIntoView(); + gTree.focus(); +}); + +// The rows should be visible and checked by default. +add_task(async function visible() { + await checkRowVisibility(true); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.equal( + gTree.view.getCellValue(row, gTree.columns.getNamedColumn("engineShown")), + "true", + "Row is checked initially" + ); + }); +}); + +// Toggling the browser.urlbar.shortcuts.* prefs should toggle the corresponding +// checkboxes in the rows. +add_task(async function syncFromPrefs() { + let col = gTree.columns.getNamedColumn("engineShown"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked initially" + ); + await SpecialPowers.pushPrefEnv({ + set: [[getUrlbarPrefName(shortcut.pref), false]], + }); + Assert.equal( + gTree.view.getCellValue(row, col), + "false", + "Row is unchecked after disabling pref" + ); + await SpecialPowers.popPrefEnv(); + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked after re-enabling pref" + ); + }); +}); + +// Pressing the space key while a row is selected should toggle its checkbox +// and pref. +add_task(async function syncToPrefs_spaceKey() { + let col = gTree.columns.getNamedColumn("engineShown"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + UrlbarPrefs.get(shortcut.pref), + "Sanity check: Pref is enabled initially" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked initially" + ); + gTree.view.selection.select(row); + EventUtils.synthesizeKey(" ", {}, gTree.ownerGlobal); + Assert.ok( + !UrlbarPrefs.get(shortcut.pref), + "Pref is disabled after pressing space key" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "false", + "Row is unchecked after pressing space key" + ); + Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref)); + }); +}); + +// Clicking the checkbox in a local shortcut row should toggle the checkbox and +// pref. +add_task(async function syncToPrefs_click() { + let col = gTree.columns.getNamedColumn("engineShown"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + UrlbarPrefs.get(shortcut.pref), + "Sanity check: Pref is enabled initially" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked initially" + ); + + let rect = gTree.getCoordsForCellItem(row, col, "cell"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + EventUtils.synthesizeMouse(gTree.body, x, y, {}, gTree.ownerGlobal); + + Assert.ok( + !UrlbarPrefs.get(shortcut.pref), + "Pref is disabled after clicking checkbox" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "false", + "Row is unchecked after clicking checkbox" + ); + Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref)); + }); +}); + +// The keyword column should not be editable according to isEditable(). +add_task(async function keywordNotEditable_isEditable() { + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + !gTree.view.isEditable( + row, + gTree.columns.getNamedColumn("engineKeyword") + ), + "Keyword column is not editable" + ); + }); +}); + +// Pressing the enter key while a row is selected shouldn't allow the keyword to +// be edited. +add_task(async function keywordNotEditable_enterKey() { + let col = gTree.columns.getNamedColumn("engineKeyword"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + shortcut.restrict, + "Sanity check: Shortcut restriction char is non-empty" + ); + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Sanity check: Keyword column has correct restriction char initially" + ); + + gTree.view.selection.select(row); + EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); + EventUtils.sendString("newkeyword"); + EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); + + // Wait a moment to allow for any possible asynchronicity. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Keyword column is still restriction char" + ); + }); +}); + +// Double-clicking the keyword column shouldn't allow the keyword to be edited. +add_task(async function keywordNotEditable_click() { + let col = gTree.columns.getNamedColumn("engineKeyword"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + shortcut.restrict, + "Sanity check: Shortcut restriction char is non-empty" + ); + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Sanity check: Keyword column has correct restriction char initially" + ); + + let rect = gTree.getCoordsForCellItem(row, col, "text"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + + let promise = BrowserTestUtils.waitForEvent(gTree, "dblclick"); + + // Click once to select the row. + EventUtils.synthesizeMouse( + gTree.body, + x, + y, + { clickCount: 1 }, + gTree.ownerGlobal + ); + + // Now double-click the keyword column. + EventUtils.synthesizeMouse( + gTree.body, + x, + y, + { clickCount: 2 }, + gTree.ownerGlobal + ); + + await promise; + + EventUtils.sendString("newkeyword"); + EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); + + // Wait a moment to allow for any possible asynchronicity. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Keyword column is still restriction char" + ); + }); +}); + +/** + * Asserts that the engine and local shortcut rows are present in the tree. + */ +async function checkRowVisibility() { + let engines = await Services.search.getVisibleEngines(); + + Assert.equal( + gTree.view.rowCount, + engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "Expected number of tree rows" + ); + + // Check the engine rows. + for (let row = 0; row < engines.length; row++) { + let engine = engines[row]; + let text = gTree.view.getCellText( + row, + gTree.columns.getNamedColumn("engineName") + ); + Assert.equal( + text, + engine.name, + `Sanity check: Tree row ${row} has expected engine name` + ); + } + + // Check the shortcut rows. + await forEachLocalShortcutRow(async (row, shortcut) => { + let text = gTree.view.getCellText( + row, + gTree.columns.getNamedColumn("engineName") + ); + let name = UrlbarUtils.getResultSourceName(shortcut.source); + let l10nName = await gTree.ownerDocument.l10n.formatValue( + `urlbar-search-mode-${name}` + ); + Assert.ok(l10nName, "Sanity check: l10n name is non-empty"); + Assert.equal(text, l10nName, `Tree row ${row} has expected shortcut name`); + }); +} + +/** + * Calls a callback for each local shortcut row in the tree. + * + * @param {function} callback + * Called for each local shortcut row like: callback(rowIndex, shortcutObject) + */ +async function forEachLocalShortcutRow(callback) { + let engines = await Services.search.getVisibleEngines(); + for (let i = 0; i < UrlbarUtils.LOCAL_SEARCH_MODES.length; i++) { + let shortcut = UrlbarUtils.LOCAL_SEARCH_MODES[i]; + let row = engines.length + i; + // These tests assume LOCAL_SEARCH_MODES are enabled, this can be removed + // when we enable QuickActions. We cant just enable the pref in browser.ini + // as this test calls clearUserPref. + if (shortcut.pref == "shortcuts.quickactions") { + continue; + } + await callback(row, shortcut); + } +} + +/** + * Prepends the `browser.urlbar.` branch to the given relative pref. + * + * @param {string} relativePref + * A pref name relative to the `browser.urlbar.`. + * @returns {string} + * The full pref name with `browser.urlbar.` prepended. + */ +function getUrlbarPrefName(relativePref) { + return `browser.urlbar.${relativePref}`; +} diff --git a/browser/components/preferences/tests/browser_moreFromMozilla.js b/browser/components/preferences/tests/browser_moreFromMozilla.js new file mode 100644 index 0000000000..0c9b6e2b88 --- /dev/null +++ b/browser/components/preferences/tests/browser_moreFromMozilla.js @@ -0,0 +1,380 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +let { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +async function clearPolicies() { + // Ensure no active policies are set + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); +} + +// The Relay promo is only shown if the default FxA instance is detected, and +// tests override it to a dummy address, so we need to make the dummy address +// appear like it's the default (using the actual default instance might cause a +// remote connection, crashing the test harness). +add_setup(mockDefaultFxAInstance); + +add_task(async function testDefaultUIWithoutTemplatePref() { + await clearPolicies(); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + ok(moreFromMozillaCategory, "The category exists"); + ok(!moreFromMozillaCategory.hidden, "The category is not hidden"); + + moreFromMozillaCategory.click(); + + let productCards = doc.querySelectorAll(".mozilla-product-item.simple"); + Assert.ok(productCards, "Default UI uses simple template"); + Assert.equal(productCards.length, 3, "3 product cards displayed"); + + const expectedUrl = "https://www.mozilla.org/firefox/browsers/mobile/"; + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, url => + url.startsWith(expectedUrl) + ); + let mobileLink = doc.getElementById("default-fxMobile"); + mobileLink.click(); + let openedTab = await tabOpened; + Assert.ok(gBrowser.selectedBrowser.documentURI.spec.startsWith(expectedUrl)); + + let searchParams = new URL(gBrowser.selectedBrowser.documentURI.spec) + .searchParams; + Assert.equal( + searchParams.get("utm_source"), + "about-prefs", + "expected utm_source sent" + ); + Assert.equal( + searchParams.get("utm_campaign"), + "morefrommozilla", + "utm_campaign set" + ); + Assert.equal( + searchParams.get("utm_medium"), + "firefox-desktop", + "utm_medium set" + ); + Assert.equal( + searchParams.get("utm_content"), + "default-global", + "default utm_content set" + ); + Assert.ok( + !searchParams.has("entrypoint_variation"), + "entrypoint_variation should not be set" + ); + Assert.ok( + !searchParams.has("entrypoint_experiment"), + "entrypoint_experiment should not be set" + ); + BrowserTestUtils.removeTab(openedTab); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testDefaulEmailClick() { + await clearPolicies(); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + moreFromMozillaCategory.click(); + + const expectedUrl = "https://www.mozilla.org/firefox/mobile/get-app/?v=mfm"; + let sendEmailLink = doc.getElementById("default-qr-code-send-email"); + + Assert.ok( + sendEmailLink.href.startsWith(expectedUrl), + `Expected URL ${sendEmailLink.href}` + ); + + let searchParams = new URL(sendEmailLink.href).searchParams; + Assert.equal(searchParams.get("v"), "mfm", "expected send email param set"); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Test that we don't show moreFromMozilla pane when it's disabled. + */ +add_task(async function testwhenPrefDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", false]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + ok(moreFromMozillaCategory, "The category exists"); + ok(moreFromMozillaCategory.hidden, "The category is hidden"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_event_telemetry() { + Services.telemetry.clearEvents(); + Services.telemetry.setEventRecordingEnabled("aboutpreferences", true); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", true]], + }); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + + let clickedPromise = BrowserTestUtils.waitForEvent( + moreFromMozillaCategory, + "click" + ); + moreFromMozillaCategory.click(); + await clickedPromise; + + TelemetryTestUtils.assertEvents( + [["aboutpreferences", "show", "initial", "paneGeneral"]], + { category: "aboutpreferences", method: "show", object: "initial" }, + { clear: false } + ); + TelemetryTestUtils.assertEvents( + [["aboutpreferences", "show", "click", "paneMoreFromMozilla"]], + { category: "aboutpreferences", method: "show", object: "click" }, + { clear: false } + ); + TelemetryTestUtils.assertNumberOfEvents(2); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_simple_template() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + + moreFromMozillaCategory.click(); + + let productCards = doc.querySelectorAll(".mozilla-product-item"); + Assert.ok(productCards, "The product cards from simple template found"); + Assert.equal(productCards.length, 3, "3 product cards displayed"); + + let qrCodeButtons = doc.querySelectorAll('.qr-code-box[hidden="false"]'); + Assert.equal(qrCodeButtons.length, 1, "1 qr-code box displayed"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_clickBtnVPN() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll(".mozilla-product-item.simple"); + Assert.ok(productCards, "Simple template loaded"); + + const expectedUrl = "https://www.mozilla.org/products/vpn/"; + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, url => + url.startsWith(expectedUrl) + ); + + let vpnButton = doc.getElementById("simple-mozillaVPN"); + vpnButton.click(); + + let openedTab = await tabOpened; + Assert.ok(gBrowser.selectedBrowser.documentURI.spec.startsWith(expectedUrl)); + + let searchParams = new URL(gBrowser.selectedBrowser.documentURI.spec) + .searchParams; + Assert.equal( + searchParams.get("utm_source"), + "about-prefs", + "expected utm_source sent" + ); + Assert.equal( + searchParams.get("utm_campaign"), + "morefrommozilla", + "utm_campaign set" + ); + Assert.equal( + searchParams.get("utm_medium"), + "firefox-desktop", + "utm_medium set" + ); + Assert.equal( + searchParams.get("utm_content"), + "fxvt-113-a-global", + "utm_content set" + ); + Assert.equal( + searchParams.get("entrypoint_experiment"), + "morefrommozilla-experiment-1846", + "entrypoint_experiment set" + ); + Assert.equal( + searchParams.get("entrypoint_variation"), + "treatment-simple", + "entrypoint_variation set" + ); + BrowserTestUtils.removeTab(openedTab); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_aboutpreferences_clickBtnMobile() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll("vbox.simple"); + Assert.ok(productCards, "Simple template loaded"); + + const expectedUrl = "https://www.mozilla.org/firefox/browsers/mobile/"; + + let mobileUrl = new URL(doc.getElementById("simple-fxMobile").href); + + Assert.ok(mobileUrl.href.startsWith(expectedUrl)); + + let searchParams = mobileUrl.searchParams; + Assert.equal( + searchParams.get("utm_source"), + "about-prefs", + "expected utm_source sent" + ); + Assert.equal( + searchParams.get("utm_campaign"), + "morefrommozilla", + "utm_campaign set" + ); + Assert.equal( + searchParams.get("utm_medium"), + "firefox-desktop", + "utm_medium set" + ); + Assert.equal( + searchParams.get("utm_content"), + "fxvt-113-a-global", + "default-global", + "utm_content set" + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_aboutpreferences_search() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", true]], + }); + + await openPreferencesViaOpenPreferencesAPI(null, { + leaveOpen: true, + }); + + await runSearchInput("Relay"); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll(".mozilla-product-item"); + Assert.equal(productCards.length, 3, "All products in the group are found"); + let [mobile, vpn, relay] = productCards; + Assert.ok(BrowserTestUtils.is_hidden(mobile), "Mobile hidden"); + Assert.ok(BrowserTestUtils.is_hidden(vpn), "VPN hidden"); + Assert.ok(BrowserTestUtils.is_visible(relay), "Relay shown"); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_aboutpreferences_clickBtnRelay() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", true]], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let expectedUrl = new URL("https://relay.firefox.com"); + expectedUrl.searchParams.set("utm_source", "about-prefs"); + expectedUrl.searchParams.set("utm_campaign", "morefrommozilla"); + expectedUrl.searchParams.set("utm_medium", "firefox-desktop"); + expectedUrl.searchParams.set("utm_content", "fxvt-113-a-global"); + expectedUrl.searchParams.set( + "entrypoint_experiment", + "morefrommozilla-experiment-1846" + ); + expectedUrl.searchParams.set("entrypoint_variation", "treatment-simple"); + + let tabOpened = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedUrl.toString(), + gBrowser, + channel => { + Assert.equal( + channel.originalURI.spec, + expectedUrl.toString(), + "URL matched" + ); + return true; + } + ); + doc.getElementById("simple-firefoxRelay").click(); + + await tabOpened; + BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_moreFromMozilla_locales.js b/browser/components/preferences/tests/browser_moreFromMozilla_locales.js new file mode 100644 index 0000000000..404e22b3ea --- /dev/null +++ b/browser/components/preferences/tests/browser_moreFromMozilla_locales.js @@ -0,0 +1,331 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +let { Region } = ChromeUtils.importESModule( + "resource://gre/modules/Region.sys.mjs" +); + +const initialHomeRegion = Region._home; +const initialCurrentRegion = Region._current; + +// Helper to run tests for specific regions +async function setupRegions(home, current) { + Region._setHomeRegion(home || ""); + Region._setCurrentRegion(current || ""); +} + +function setLocale(language) { + Services.locale.availableLocales = [language]; + Services.locale.requestedLocales = [language]; +} + +async function clearPolicies() { + // Ensure no active policies are set + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); +} + +async function getPromoCards() { + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let vpnPromoCard = doc.getElementById("mozilla-vpn"); + let mobileCard = doc.getElementById("firefox-mobile"); + let relayPromoCard = doc.getElementById("firefox-relay"); + + return { + vpnPromoCard, + mobileCard, + relayPromoCard, + }; +} + +let mockFxA, unmockFxA; + +// The Relay promo is only shown if the default FxA instance is detected, and +// tests override it to a dummy address, so we need to make the dummy address +// appear like it's the default (using the actual default instance might cause a +// remote connection, crashing the test harness). +add_setup(async function () { + let { mock, unmock } = await mockDefaultFxAInstance(); + mockFxA = mock; + unmockFxA = unmock; +}); + +add_task(async function test_VPN_promo_enabled() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.vpn_promo.enabled", true], + ], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(vpnPromoCard, "The VPN promo is visible"); + ok(mobileCard, "The Mobile promo is visible"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_disabled() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.enabled", false]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + Services.prefs.clearUserPref("browser.vpn_promo.enabled"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_disallowed_home_region() { + await clearPolicies(); + const disallowedRegion = "SY"; + + setupRegions(disallowedRegion); + + // Promo should not show in disallowed regions even when vpn_promo pref is enabled + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.enabled", true]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_illegal_home_region() { + await clearPolicies(); + const illegalRegion = "CN"; + + setupRegions(illegalRegion); + + // Promo should not show in illegal regions even if the list of disallowed regions is somehow altered (though changing this preference is blocked) + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.disallowedRegions", "SY, CU"]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_disallowed_current_region() { + await clearPolicies(); + const allowedRegion = "US"; + const disallowedRegion = "SY"; + + setupRegions(allowedRegion, disallowedRegion); + + // Promo should not show in disallowed regions even when vpn_promo pref is enabled + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.enabled", true]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_illegal_current_region() { + await clearPolicies(); + const allowedRegion = "US"; + const illegalRegion = "CN"; + + setupRegions(allowedRegion, illegalRegion); + + // Promo should not show in illegal regions even if the list of disallowed regions is somehow altered (though changing this preference is blocked) + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.disallowedRegions", "SY, CU"]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_partnerCNRepack() { + let defaultBranch = Services.prefs.getDefaultBranch(null); + defaultBranch.setCharPref("distribution.id", "MozillaOnline"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll("vbox.simple"); + Assert.ok(productCards, "Simple template loaded"); + + const expectedUrl = "https://www.firefox.com.cn/browsers/mobile/"; + + let link = doc.getElementById("simple-fxMobile"); + Assert.ok(link.getAttribute("href").startsWith(expectedUrl)); + + defaultBranch.setCharPref("distribution.id", ""); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_send_to_device_email_link_for_supported_locale() { + // Email is supported for Brazilian Portuguese + const supportedLocale = "pt-BR"; + const initialLocale = Services.locale.appLocaleAsBCP47; + setLocale(supportedLocale); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla.template", "simple"]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let emailLink = doc.getElementById("simple-qr-code-send-email"); + + ok(!BrowserTestUtils.is_hidden(emailLink), "Email link should be visible"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + setLocale(initialLocale); // revert changes to language +}); + +add_task( + async function test_send_to_device_email_link_for_unsupported_locale() { + // Email is not supported for Afrikaans + const unsupportedLocale = "af"; + const initialLocale = Services.locale.appLocaleAsBCP47; + setLocale(unsupportedLocale); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla.template", "simple"]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let emailLink = doc.getElementById("simple-qr-code-send-email"); + + ok(BrowserTestUtils.is_hidden(emailLink), "Email link should be hidden"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + setLocale(initialLocale); // revert changes to language + } +); + +add_task( + async function test_VPN_promo_in_unsupported_current_region_with_supported_home_region() { + await clearPolicies(); + const supportedRegion = "US"; + const unsupportedRegion = "LY"; + + setupRegions(supportedRegion, unsupportedRegion); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(vpnPromoCard, "The VPN promo is visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task( + async function test_VPN_promo_in_supported_current_region_with_unsupported_home_region() { + await clearPolicies(); + const supportedRegion = "US"; + const unsupportedRegion = "LY"; + + setupRegions(unsupportedRegion, supportedRegion); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(vpnPromoCard, "The VPN promo is visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task(async function test_VPN_promo_with_active_enterprise_policy() { + // set up an arbitrary enterprise policy + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + EnableTrackingProtection: { + Value: true, + }, + }, + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + await clearPolicies(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_relay_promo_with_supported_fxa_server() { + await clearPolicies(); + + let { relayPromoCard } = await getPromoCards(); + ok(relayPromoCard, "The Relay promo is visible"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_relay_promo_with_unsupported_fxa_server() { + await clearPolicies(); + // Set the default pref value to something other than the current value so it + // will appear to be user-set and treated as invalid (actually setting the + // pref would cause a remote connection and crash the test harness) + unmockFxA(); + + let { relayPromoCard } = await getPromoCards(); + ok(!relayPromoCard, "The Relay promo is not visible"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + mockFxA(); +}); diff --git a/browser/components/preferences/tests/browser_newtab_menu.js b/browser/components/preferences/tests/browser_newtab_menu.js new file mode 100644 index 0000000000..774c9dd756 --- /dev/null +++ b/browser/components/preferences/tests/browser_newtab_menu.js @@ -0,0 +1,38 @@ +add_task(async function newtabPreloaded() { + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + const { contentDocument: doc, contentWindow } = gBrowser; + function dispatchMenuItemCommand(menuItem) { + const cmdEvent = doc.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + contentWindow, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + menuItem.dispatchEvent(cmdEvent); + } + + const menuHome = doc.querySelector(`#newTabMode menuitem[value="0"]`); + const menuBlank = doc.querySelector(`#newTabMode menuitem[value="1"]`); + ok(menuHome.selected, "The first item, Home (default), is selected."); + ok(NewTabPagePreloading.enabled, "Default Home allows preloading."); + + dispatchMenuItemCommand(menuBlank); + ok(menuBlank.selected, "The second item, Blank, is selected."); + ok(!NewTabPagePreloading.enabled, "Non-Home prevents preloading."); + + dispatchMenuItemCommand(menuHome); + ok(menuHome.selected, "The first item, Home, is selected again."); + ok(NewTabPagePreloading.enabled, "Default Home allows preloading again."); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_notifications_do_not_disturb.js b/browser/components/preferences/tests/browser_notifications_do_not_disturb.js new file mode 100644 index 0000000000..afc31b9041 --- /dev/null +++ b/browser/components/preferences/tests/browser_notifications_do_not_disturb.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +registerCleanupFunction(function () { + while (gBrowser.tabs[1]) { + gBrowser.removeTab(gBrowser.tabs[1]); + } +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected"); + + let doc = gBrowser.contentDocument; + let notificationsDoNotDisturbBox = doc.getElementById( + "notificationsDoNotDisturbBox" + ); + if (notificationsDoNotDisturbBox.hidden) { + todo(false, "Do not disturb is not available on this platform"); + return; + } + + let alertService; + try { + alertService = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIAlertsDoNotDisturb); + } catch (ex) { + ok(true, "Do not disturb is not available on this platform: " + ex.message); + return; + } + + let checkbox = doc.getElementById("notificationsDoNotDisturb"); + ok(!checkbox.checked, "Checkbox should not be checked by default"); + ok( + !alertService.manualDoNotDisturb, + "Do not disturb should be off by default" + ); + + let checkboxChanged = BrowserTestUtils.waitForEvent(checkbox, "command"); + checkbox.click(); + await checkboxChanged; + ok( + alertService.manualDoNotDisturb, + "Do not disturb should be enabled when checked" + ); + + checkboxChanged = BrowserTestUtils.waitForEvent(checkbox, "command"); + checkbox.click(); + await checkboxChanged; + ok( + !alertService.manualDoNotDisturb, + "Do not disturb should be disabled when unchecked" + ); +}); diff --git a/browser/components/preferences/tests/browser_open_download_preferences.js b/browser/components/preferences/tests/browser_open_download_preferences.js new file mode 100644 index 0000000000..794d2ebb05 --- /dev/null +++ b/browser/components/preferences/tests/browser_open_download_preferences.js @@ -0,0 +1,288 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +async function selectPdfCategoryItem() { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + info("Preferences page opened on the general pane."); + + await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList; + info("Apps list loaded."); + + let win = gBrowser.selectedBrowser.contentWindow; + let container = win.document.getElementById("handlersView"); + let pdfCategory = container.querySelector( + "richlistitem[type='application/pdf']" + ); + + pdfCategory.closest("richlistbox").selectItem(pdfCategory); + Assert.ok(pdfCategory.selected, "Should be able to select our item."); + + return pdfCategory; +} + +async function selectItemInPopup(item, list) { + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + // Synthesizing the mouse on the .actionsMenu menulist somehow just selects + // the top row. Probably something to do with the multiple layers of anon + // content - workaround by using the `.open` setter instead. + list.open = true; + await popupShown; + let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + + item.click(); + popup.hidePopup(); + await popupHidden; + return item; +} + +function downloadHadFinished(publicList) { + return new Promise(resolve => { + publicList.addView({ + onDownloadChanged(download) { + if (download.succeeded || download.error) { + publicList.removeView(this); + resolve(download); + } + }, + }); + }); +} + +async function removeTheFile(download) { + Assert.ok( + await IOUtils.exists(download.target.path), + "The file should have been downloaded." + ); + + try { + info("removing " + download.target.path); + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } catch (ex) { + info("The file " + download.target.path + " is not removed, " + ex); + } +} + +add_task(async function alwaysAskPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let alwaysAskItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.alwaysAsk}']` + ); + + await selectItemInPopup(alwaysAskItem, list); + Assert.equal( + list.selectedItem, + alwaysAskItem, + "Should have selected 'always ask' for pdf" + ); + let alwaysAskBeforeHandling = HandlerServiceTestUtils.getHandlerInfo( + pdfCategory.getAttribute("type") + ).alwaysAskBeforeHandling; + Assert.ok( + alwaysAskBeforeHandling, + "Should have turned on 'always asking before handling'" + ); + + let domWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + let domWindow = await domWindowPromise; + let dialog = domWindow.document.querySelector("#unknownContentType"); + let button = dialog.getButton("cancel"); + + await TestUtils.waitForCondition( + () => !button.disabled, + "Wait for Cancel button to get enabled" + ); + Assert.ok(dialog, "Dialog should be shown"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function handleInternallyPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let handleInternallyItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']` + ); + + await selectItemInPopup(handleInternallyItem, list); + Assert.equal( + list.selectedItem, + handleInternallyItem, + "Should have selected 'handle internally' for pdf" + ); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + await ContentTask.spawn(loadingTab.linkedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.readyState == "complete" + ); + Assert.ok( + content.document.querySelector("div#viewer"), + "document content has viewer UI" + ); + }); + + BrowserTestUtils.removeTab(loadingTab); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function saveToDiskPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let saveToDiskItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.saveToDisk}']` + ); + + await selectItemInPopup(saveToDiskItem, list); + Assert.equal( + list.selectedItem, + saveToDiskItem, + "Should have selected 'save to disk' for pdf" + ); + + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + + let downloadFinishedPromise = downloadHadFinished(publicList); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + let download = await downloadFinishedPromise; + BrowserTestUtils.removeTab(loadingTab); + + await removeTheFile(download); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function useSystemDefaultPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let useSystemDefaultItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.useSystemDefault}']` + ); + + // Whether there's a "use default" item depends on the OS, there might not be a system default viewer. + if (!useSystemDefaultItem) { + info( + "No 'Use default' item, so no testing for setting 'use system default' preference" + ); + gBrowser.removeCurrentTab(); + return; + } + + await selectItemInPopup(useSystemDefaultItem, list); + Assert.equal( + list.selectedItem, + useSystemDefaultItem, + "Should have selected 'use system default' for pdf" + ); + + let oldLaunchFile = DownloadIntegration.launchFile; + + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + + let downloadFinishedPromise = downloadHadFinished(publicList); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + info("Downloading had finished"); + let download = await downloadFinishedPromise; + + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + + DownloadIntegration.launchFile = oldLaunchFile; + + await removeTheFile(download); + + BrowserTestUtils.removeTab(loadingTab); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_open_migration_wizard.js b/browser/components/preferences/tests/browser_open_migration_wizard.js new file mode 100644 index 0000000000..c2c18b35ef --- /dev/null +++ b/browser/components/preferences/tests/browser_open_migration_wizard.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the "Import Data" button in the "Import Browser Data" section of + * the General pane of about:preferences launches the Migration Wizard. + */ +add_task(async function test_open_migration_wizard() { + const BUTTON_ID = "data-migration"; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#general" }, + async function (browser) { + let button = browser.contentDocument.getElementById(BUTTON_ID); + + // First, we'll test the legacy Migration Wizard. + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.content-modal.enabled", false]], + }); + + let migrationWizardWindow = BrowserTestUtils.domWindowOpenedAndLoaded( + null, + win => { + let type = win.document.documentElement.getAttribute("windowtype"); + if (type == "Browser:MigrationWizard") { + Assert.ok(true, "Saw legacy Migration Wizard window open."); + return true; + } + + return false; + } + ); + + button.click(); + let win = await migrationWizardWindow; + await BrowserTestUtils.closeWindow(win); + + // Next, we'll test the new Migration Wizard. + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.content-modal.enabled", true]], + }); + + let wizardReady = BrowserTestUtils.waitForEvent( + browser.contentWindow, + "MigrationWizard:Ready" + ); + button.click(); + await wizardReady; + Assert.ok(true, "Saw the new Migration Wizard dialog open."); + } + ); +}); diff --git a/browser/components/preferences/tests/browser_password_management.js b/browser/components/preferences/tests/browser_password_management.js new file mode 100644 index 0000000000..d84c88dc08 --- /dev/null +++ b/browser/components/preferences/tests/browser_password_management.js @@ -0,0 +1,43 @@ +"use strict"; + +const { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +var passwordsDialog; + +add_task(async function test_openPasswordManagement() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:logins"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + + let savePasswordCheckBox = doc.getElementById("savePasswords"); + Assert.ok( + !savePasswordCheckBox.checked, + "Save Password CheckBox should be unchecked by default" + ); + + let showPasswordsButton = doc.getElementById("showPasswords"); + showPasswordsButton.click(); + }); + + let tab = await tabOpenPromise; + ok(tab, "Tab opened"); + + // check telemetry events while we are in here + await LoginTestUtils.telemetry.waitForEventCount(1); + TelemetryTestUtils.assertEvents( + [["pwmgr", "open_management", "preferences"]], + { category: "pwmgr", method: "open_management" }, + { clear: true, process: "content" } + ); + + BrowserTestUtils.removeTab(tab); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_pdf_disabled.js b/browser/components/preferences/tests/browser_pdf_disabled.js new file mode 100644 index 0000000000..5b814b39e9 --- /dev/null +++ b/browser/components/preferences/tests/browser_pdf_disabled.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks that pdf always appears in the applications list even +// both a customized handler doesn't exist and when the internal viewer is +// not enabled. +add_task(async function pdfIsAlwaysPresent() { + // Try again with the pdf viewer enabled and disabled. + for (let test of ["enabled", "disabled"]) { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.disabled", test == "disabled"]], + }); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First, find the PDF item. + let pdfItem = container.querySelector( + "richlistitem[type='application/pdf']" + ); + Assert.ok(pdfItem, "pdfItem is present in handlersView when " + test); + if (pdfItem) { + pdfItem.scrollIntoView({ block: "center" }); + pdfItem.closest("richlistbox").selectItem(pdfItem); + + // Open its menu + let list = pdfItem.querySelector(".actionsMenu"); + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(list, {}, win); + await popupShown; + + let handleInternallyItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']` + ); + + is( + test == "enabled", + !!handleInternallyItem, + "handle internally is present when " + test + ); + } + + gBrowser.removeCurrentTab(); + } +}); diff --git a/browser/components/preferences/tests/browser_performance.js b/browser/components/preferences/tests/browser_performance.js new file mode 100644 index 0000000000..bd7ff70c51 --- /dev/null +++ b/browser/components/preferences/tests/browser_performance.js @@ -0,0 +1,300 @@ +const DEFAULT_HW_ACCEL_PREF = Services.prefs + .getDefaultBranch(null) + .getBoolPref("layers.acceleration.disabled"); +const DEFAULT_PROCESS_COUNT = Services.prefs + .getDefaultBranch(null) + .getIntPref("dom.ipc.processCount"); + +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF], + ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT], + ["browser.preferences.defaultPerformanceSettings.enabled", true], + ], + }); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + + useRecommendedPerformanceSettings.click(); + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + false, + "pref value should be false after clicking on checkbox" + ); + ok( + !useRecommendedPerformanceSettings.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + let allowHWAccelPref = Services.prefs.getBoolPref( + "layers.acceleration.disabled" + ); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value before clicking on checkbox" + ); + is( + allowHWAccel.checked, + !DEFAULT_HW_ACCEL_PREF, + "checkbox should show the invert of the default value" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + false, + "process count control should be enabled" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "default pref value should be default value" + ); + is( + contentProcessCount.selectedItem.value, + "" + DEFAULT_PROCESS_COUNT, + "selected item should be the default one" + ); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + !DEFAULT_HW_ACCEL_PREF, + "pref value should be opposite of the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + contentProcessCount.value = 7; + contentProcessCount.doCommand(); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + 7, + "pref value should be 7" + ); + is(contentProcessCount.selectedItem.value, "7", "selected item should be 7"); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + contentProcessCount.value = DEFAULT_PROCESS_COUNT; + contentProcessCount.doCommand(); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "pref value should be default value" + ); + is( + contentProcessCount.selectedItem.value, + "" + DEFAULT_PROCESS_COUNT, + "selected item should be default one" + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be still shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + let allowHWAccel = doc.querySelector("#allowHWAccel"); + let contentProcessCount = doc.querySelector("#contentProcessCount"); + let performanceSettings = doc.querySelector("#performanceSettings"); + + useRecommendedPerformanceSettings.click(); + allowHWAccel.click(); + contentProcessCount.value = 7; + contentProcessCount.doCommand(); + useRecommendedPerformanceSettings.click(); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + is( + performanceSettings.hidden, + true, + "performance settings section should be still shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let performanceSettings = doc.querySelector("#performanceSettings"); + + is( + performanceSettings.hidden, + true, + "performance settings section should not be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + false + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + Services.prefs.setIntPref("dom.ipc.processCount", 7); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + 7, + "pref value should be 7" + ); + is(contentProcessCount.selectedItem.value, "7", "selected item should be 7"); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + Services.prefs.setBoolPref("layers.acceleration.disabled", true); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + is( + Services.prefs.getBoolPref("layers.acceleration.disabled"), + true, + "pref value is false" + ); + ok(!allowHWAccel.checked, "checkbox should not be checked"); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_performance_content_process_limit.js b/browser/components/preferences/tests/browser_performance_content_process_limit.js new file mode 100644 index 0000000000..7ac5c354bd --- /dev/null +++ b/browser/components/preferences/tests/browser_performance_content_process_limit.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.defaultPerformanceSettings.enabled", false]], + }); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let limitContentProcess = doc.querySelector("#limitContentProcess"); + is( + limitContentProcess.hidden, + Services.appinfo.fissionAutostart, + "Limit Content Process should be hidden if fission is enabled and shown if it is not." + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.hidden, + Services.appinfo.fissionAutostart, + "Limit Content Count should be hidden if fission is enabled and shown if it is not." + ); + + let contentProcessCountEnabledDescription = doc.querySelector( + "#contentProcessCountEnabledDescription" + ); + is( + contentProcessCountEnabledDescription.hidden, + Services.appinfo.fissionAutostart, + "Limit Content Process Enabled Description should be hidden if fission is enabled and shown if it is not." + ); + + let contentProcessCountDisabledDescription = doc.querySelector( + "#contentProcessCountDisabledDescription" + ); + is( + contentProcessCountDisabledDescription.hidden, + Services.appinfo.fissionAutostart || + Services.appinfo.browserTabsRemoteAutostart, + "Limit Content Process Disabled Description should be shown if e10s is disabled, and hidden otherwise." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_performance_e10srollout.js b/browser/components/preferences/tests/browser_performance_e10srollout.js new file mode 100644 index 0000000000..1c2c57b6e7 --- /dev/null +++ b/browser/components/preferences/tests/browser_performance_e10srollout.js @@ -0,0 +1,164 @@ +const DEFAULT_HW_ACCEL_PREF = Services.prefs + .getDefaultBranch(null) + .getBoolPref("layers.acceleration.disabled"); +const DEFAULT_PROCESS_COUNT = Services.prefs + .getDefaultBranch(null) + .getIntPref("dom.ipc.processCount"); + +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF], + ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT], + ["browser.preferences.defaultPerformanceSettings.enabled", true], + ], + }); +}); + +add_task(async function testPrefsAreDefault() { + Services.prefs.setIntPref("dom.ipc.processCount", DEFAULT_PROCESS_COUNT); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + + useRecommendedPerformanceSettings.click(); + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + false, + "pref value should be false after clicking on checkbox" + ); + ok( + !useRecommendedPerformanceSettings.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + false, + "process count control should be enabled" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "default pref should be default value" + ); + is( + contentProcessCount.selectedItem.value, + "" + DEFAULT_PROCESS_COUNT, + "selected item should be the default one" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + Services.prefs.clearUserPref("dom.ipc.processCount"); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); +}); + +add_task(async function testPrefsSetByUser() { + const kNewCount = DEFAULT_PROCESS_COUNT - 2; + + Services.prefs.setIntPref("dom.ipc.processCount", kNewCount); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + false + ); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + false, + "process count control should be enabled" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + kNewCount, + "process count should be the set value" + ); + is( + contentProcessCount.selectedItem.value, + "" + kNewCount, + "selected item should be the set one" + ); + + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + useRecommendedPerformanceSettings.click(); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true after clicking on checkbox" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "process count should be default value" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + Services.prefs.clearUserPref("dom.ipc.processCount"); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); +}); diff --git a/browser/components/preferences/tests/browser_performance_non_e10s.js b/browser/components/preferences/tests/browser_performance_non_e10s.js new file mode 100644 index 0000000000..169afcaaa6 --- /dev/null +++ b/browser/components/preferences/tests/browser_performance_non_e10s.js @@ -0,0 +1,210 @@ +const DEFAULT_HW_ACCEL_PREF = Services.prefs + .getDefaultBranch(null) + .getBoolPref("layers.acceleration.disabled"); +const DEFAULT_PROCESS_COUNT = Services.prefs + .getDefaultBranch(null) + .getIntPref("dom.ipc.processCount"); + +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF], + ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT], + ["browser.preferences.defaultPerformanceSettings.enabled", true], + ], + }); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + + useRecommendedPerformanceSettings.click(); + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + false, + "pref value should be false after clicking on checkbox" + ); + ok( + !useRecommendedPerformanceSettings.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + let allowHWAccelPref = Services.prefs.getBoolPref( + "layers.acceleration.disabled" + ); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value before clicking on checkbox" + ); + is( + allowHWAccel.checked, + !DEFAULT_HW_ACCEL_PREF, + "checkbox should show the invert of the default value" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + true, + "process count control should be disabled" + ); + + let contentProcessCountEnabledDescription = doc.querySelector( + "#contentProcessCountEnabledDescription" + ); + is( + contentProcessCountEnabledDescription.hidden, + true, + "process count enabled description should be hidden" + ); + + let contentProcessCountDisabledDescription = doc.querySelector( + "#contentProcessCountDisabledDescription" + ); + is( + contentProcessCountDisabledDescription.hidden, + false, + "process count enabled description should be shown" + ); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + !DEFAULT_HW_ACCEL_PREF, + "pref value should be opposite of the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be still shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let performanceSettings = doc.querySelector("#performanceSettings"); + + is( + performanceSettings.hidden, + true, + "performance settings section should not be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + false + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + Services.prefs.setBoolPref("layers.acceleration.disabled", true); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + is( + Services.prefs.getBoolPref("layers.acceleration.disabled"), + true, + "pref value is false" + ); + ok(!allowHWAccel.checked, "checkbox should not be checked"); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js b/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js new file mode 100644 index 0000000000..829f897b72 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +const _checkAndOpenCookiesDialog = async doc => { + let cookieExceptionsButton = doc.getElementById("cookieExceptions"); + ok(cookieExceptionsButton, "cookieExceptionsButton found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + cookieExceptionsButton.click(); + let dialog = await dialogPromise; + return dialog; +}; + +const _checkCookiesDialog = (dialog, buttonIds) => { + ok(dialog, "dialog loaded"); + let urlLabel = dialog.document.getElementById("urlLabel"); + ok(!urlLabel.hidden, "urlLabel should be visible"); + let url = dialog.document.getElementById("url"); + ok(!url.hidden, "url should be visible"); + for (let buttonId of buttonIds) { + let buttonDialog = dialog.document.getElementById(buttonId); + ok(buttonDialog, "blockButtonDialog found"); + is( + buttonDialog.hasAttribute("disabled"), + true, + "If the user hasn't added an url the button shouldn't be clickable" + ); + } + return dialog; +}; + +const _addWebsiteAddressToPermissionBox = ( + websiteAddress, + dialog, + buttonId +) => { + let url = dialog.document.getElementById("url"); + let buttonDialog = dialog.document.getElementById(buttonId); + url.value = websiteAddress; + url.dispatchEvent(new Event("input", { bubbles: true })); + is( + buttonDialog.hasAttribute("disabled"), + false, + "When the user add an url the button should be clickable" + ); + buttonDialog.click(); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let children = permissionsBox.getElementsByAttribute("origin", "*"); + is(!children.length, false, "Website added in url should be in the list"); +}; + +const _checkIfPermissionsWereAdded = (dialog, expectedResult) => { + let permissionsBox = dialog.document.getElementById("permissionsBox"); + for (let website of expectedResult) { + let elements = permissionsBox.getElementsByAttribute("origin", website); + is(elements.length, 1, "It should find only one coincidence"); + } +}; + +const _removesAllSitesInPermissionBox = dialog => { + let removeAllWebsitesButton = dialog.document.getElementById( + "removeAllPermissions" + ); + ok(removeAllWebsitesButton, "removeAllWebsitesButton found"); + is( + removeAllWebsitesButton.hasAttribute("disabled"), + false, + "There should be websites in the list" + ); + removeAllWebsitesButton.click(); +}; + +add_task(async function checkCookiePermissions() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + let buttonIds = ["btnBlock", "btnCookieSession", "btnAllow"]; + + let dialog = await _checkAndOpenCookiesDialog(doc); + _checkCookiesDialog(dialog, buttonIds); + + let tests = [ + { + inputWebsite: "google.com", + expectedResult: ["http://google.com", "https://google.com"], + }, + { + inputWebsite: "https://google.com", + expectedResult: ["https://google.com"], + }, + { + inputWebsite: "http://", + expectedResult: ["http://http", "https://http"], + }, + { + inputWebsite: "s3.eu-central-1.amazonaws.com", + expectedResult: [ + "http://s3.eu-central-1.amazonaws.com", + "https://s3.eu-central-1.amazonaws.com", + ], + }, + { + inputWebsite: "file://", + expectedResult: ["file:///"], + }, + { + inputWebsite: "about:config", + expectedResult: ["about:config"], + }, + ]; + + for (let buttonId of buttonIds) { + for (let test of tests) { + _addWebsiteAddressToPermissionBox(test.inputWebsite, dialog, buttonId); + _checkIfPermissionsWereAdded(dialog, test.expectedResult); + _removesAllSitesInPermissionBox(dialog); + } + } + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_permissions_dialog.js b/browser/components/preferences/tests/browser_permissions_dialog.js new file mode 100644 index 0000000000..3a5e0f95c2 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_dialog.js @@ -0,0 +1,642 @@ +"use strict"; + +/* 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 PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml"; +const URL = "http://www.example.com"; +const URI = Services.io.newURI(URL); +var sitePermissionsDialog; +let settingsButtonMap = { + "desktop-notification": "notificationSettingsButton", + speaker: "speakerSettingsButton", +}; + +function checkMenulistPermissionItem(origin, state) { + let doc = sitePermissionsDialog.document; + + let label = doc.getElementsByTagName("label")[3]; + Assert.equal(label.value, origin); + + let menulist = doc.getElementsByTagName("menulist")[0]; + Assert.equal(menulist.value, state); +} + +async function openPermissionsDialog(permissionType) { + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [settingsButtonMap[permissionType]], + function (settingsButtonId) { + let doc = content.document; + let settingsButton = doc.getElementById(settingsButtonId); + settingsButton.click(); + } + ); + + sitePermissionsDialog = await dialogOpened; + await sitePermissionsDialog.document.mozSubdialogReady; +} + +add_task(async function openSitePermissionsDialog() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openPermissionsDialog("desktop-notification"); +}); + +add_task(async function addPermission() { + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + // First item in the richlistbox contains column headers. + Assert.equal( + richlistbox.itemCount, + 0, + "Number of permission items is 0 initially" + ); + + // Add notification permission for a website. + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Observe the added permission changes in the dialog UI. + Assert.equal(richlistbox.itemCount, 1); + checkMenulistPermissionItem(URL, Services.perms.ALLOW_ACTION); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function addPermissionPrivateBrowsing() { + let privateBrowsingPrincipal = + Services.scriptSecurityManager.createContentPrincipal(URI, { + privateBrowsingId: 1, + }); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + Assert.equal( + richlistbox.itemCount, + 0, + "Number of permission items is 0 initially" + ); + + // Add a session permission for private browsing. + PermissionTestUtils.add( + privateBrowsingPrincipal, + "desktop-notification", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_SESSION + ); + + // The permission should not show in the dialog UI. + Assert.equal(richlistbox.itemCount, 0); + + PermissionTestUtils.remove(privateBrowsingPrincipal, "desktop-notification"); + + // Add a permanent permission for private browsing + // The permission manager will store it as EXPIRE_SESSION + PermissionTestUtils.add( + privateBrowsingPrincipal, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // The permission should not show in the dialog UI. + Assert.equal(richlistbox.itemCount, 0); + + PermissionTestUtils.remove(privateBrowsingPrincipal, "desktop-notification"); +}); + +add_task(async function observePermissionChange() { + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission. + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.DENY_ACTION + ); + + checkMenulistPermissionItem(URL, Services.perms.DENY_ACTION); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function observePermissionDelete() { + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + richlistbox.itemCount, + 1, + "The box contains one permission item initially" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + + Assert.equal(richlistbox.itemCount, 0); +}); + +add_task(async function onPermissionChange() { + let doc = sitePermissionsDialog.document; + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission state in the UI. + doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click(); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission state does not change before saving changes" + ); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability == Services.perms.DENY_ACTION + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function onPermissionDelete() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + richlistbox.selectItem(richlistbox.getItemAtIndex(0)); + doc.getElementById("removePermission").click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission is not deleted before saving changes" + ); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") == + null + ); +}); + +add_task(async function onAllPermissionsDelete() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + doc.getElementById("removeAllPermissions").click(); + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject(u, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permissions are not deleted before saving changes" + ); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") == + null && + PermissionTestUtils.getPermissionObject(u, "desktop-notification") == null + ); +}); + +add_task(async function onPermissionChangeAndDelete() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission state in the UI. + doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click(); + + // Remove that permission by clicking the "Remove" button. + richlistbox.selectItem(richlistbox.getItemAtIndex(0)); + doc.getElementById("removePermission").click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") == + null + ); +}); + +add_task(async function onPermissionChangeCancel() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission state in the UI. + doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click(); + + doc.querySelector("dialog").getButton("cancel").click(); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission state does not change on clicking cancel" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function onPermissionDeleteCancel() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Remove that permission by clicking the "Remove" button. + richlistbox.selectItem(richlistbox.getItemAtIndex(0)); + doc.getElementById("removePermission").click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + doc.querySelector("dialog").getButton("cancel").click(); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission state does not change on clicking cancel" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function onSearch() { + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + let searchBox = doc.getElementById("searchBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + searchBox.value = "www.example.com"; + + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + doc.getElementsByAttribute("origin", "http://www.test.com")[0], + null + ); + Assert.equal( + doc.getElementsByAttribute("origin", "http://www.example.com")[0], + richlistbox.getItemAtIndex(0) + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + PermissionTestUtils.remove(u, "desktop-notification"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function onPermissionsSort() { + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.DENY_ACTION + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + // Test default arrangement(Allow followed by Block). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.example.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.test.com" + ); + + doc.getElementById("statusCol").click(); + + // Test the rearrangement(Block followed by Allow). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.test.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.example.com" + ); + + doc.getElementById("siteCol").click(); + + // Test the rearrangement(Website names arranged in alphabhetical order). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.example.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.test.com" + ); + + doc.getElementById("siteCol").click(); + + // Test the rearrangement(Website names arranged in reverse alphabhetical order). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.test.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.example.com" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + PermissionTestUtils.remove(u, "desktop-notification"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function onPermissionDisable() { + // Enable desktop-notification permission prompts. + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.UNKNOWN + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + + // Check if the enabled state is reflected in the checkbox. + let checkbox = doc.getElementById("permissionsDisableCheckbox"); + Assert.equal(checkbox.checked, false); + + // Disable permission and click on "Cancel". + checkbox.checked = true; + doc.querySelector("dialog").getButton("cancel").click(); + + // Check that the permission is not disabled yet. + let perm = Services.prefs.getIntPref( + "permissions.default.desktop-notification" + ); + Assert.equal(perm, SitePermissions.UNKNOWN); + + // Open the dialog once again. + await openPermissionsDialog("desktop-notification"); + doc = sitePermissionsDialog.document; + + // Disable permission and save changes. + checkbox = doc.getElementById("permissionsDisableCheckbox"); + checkbox.checked = true; + doc.querySelector("dialog").getButton("accept").click(); + + // Check if the permission is now disabled. + perm = Services.prefs.getIntPref("permissions.default.desktop-notification"); + Assert.equal(perm, SitePermissions.BLOCK); + + // Open the dialog once again and check if the disabled state is still reflected in the checkbox. + await openPermissionsDialog("desktop-notification"); + doc = sitePermissionsDialog.document; + checkbox = doc.getElementById("permissionsDisableCheckbox"); + Assert.equal(checkbox.checked, true); + + // Close the dialog and clean up. + doc.querySelector("dialog").getButton("cancel").click(); + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.UNKNOWN + ); +}); + +add_task(async function checkDefaultPermissionState() { + // Set default permission state to ALLOW. + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.ALLOW + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + + // Check if the enabled state is reflected in the checkbox. + let checkbox = doc.getElementById("permissionsDisableCheckbox"); + Assert.equal(checkbox.checked, false); + + // Check the checkbox and then uncheck it. + checkbox.checked = true; + checkbox.checked = false; + + // Save changes. + doc.querySelector("dialog").getButton("accept").click(); + + // Check if the default permission state is retained (and not automatically set to SitePermissions.UNKNOWN). + let state = Services.prefs.getIntPref( + "permissions.default.desktop-notification" + ); + Assert.equal(state, SitePermissions.ALLOW); + + // Clean up. + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.UNKNOWN + ); +}); + +add_task(async function testTabBehaviour() { + // Test tab behaviour inside the permissions setting dialog when site permissions are selected. + // Only selected items in the richlistbox should be tabable for accessibility reasons. + + // Force tabfocus for all elements on OSX. + SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] }); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + + EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog); + let richlistbox = doc.getElementById("permissionsBox"); + is( + richlistbox, + doc.activeElement.closest("#permissionsBox"), + "The richlistbox is focused after pressing tab once." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown", {}, sitePermissionsDialog); + EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog); + let menulist = doc + .getElementById("permissionsBox") + .itemChildren[1].getElementsByTagName("menulist")[0]; + is( + menulist, + doc.activeElement, + "The menulist inside the selected richlistitem is focused now" + ); + + EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog); + let removeButton = doc.getElementById("removePermission"); + is( + removeButton, + doc.activeElement, + "The focus moves outside the richlistbox and onto the remove button" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + PermissionTestUtils.remove(u, "desktop-notification"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function addSpeakerPermission() { + let enabled = Services.prefs.getBoolPref("media.setsinkid.enabled", false); + let speakerRow = + gBrowser.contentDocument.getElementById("speakerSettingsRow"); + Assert.equal( + BrowserTestUtils.is_visible(speakerRow), + enabled, + "speakerRow visible" + ); + if (!enabled) { + return; + } + + await openPermissionsDialog("speaker"); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + Assert.equal( + richlistbox.itemCount, + 0, + "Number of permission items is 0 initially" + ); + // Add an allow permission for a device. + let deviceId = "DEVICE-ID"; + let devicePermissionId = `speaker^${deviceId}`; + PermissionTestUtils.add(URI, devicePermissionId, Services.perms.ALLOW_ACTION); + + // Observe the added permission changes in the dialog UI. + Assert.equal(richlistbox.itemCount, 1, "itemCount with allow"); + checkMenulistPermissionItem(URL, Services.perms.ALLOW_ACTION); + + // Check that an all-device deny permission overrides the device-specific + // allow permission. + PermissionTestUtils.add(URI, "speaker", Services.perms.DENY_ACTION); + + Assert.equal(richlistbox.itemCount, 1, "itemCount with deny and allow"); + let richlistitem = richlistbox.itemChildren[0]; + let siteStatus = richlistitem.querySelector(".website-status"); + Assert.equal( + siteStatus.value, + Services.perms.DENY_ACTION, + "website status with deny and allow" + ); + // The website status element is not a menulist because all-device allow is + // not an option. + Assert.equal(siteStatus.tagName, "hbox"); + Assert.equal(siteStatus.firstElementChild.tagName, "label"); + + PermissionTestUtils.remove(URI, devicePermissionId); + PermissionTestUtils.remove(URI, "speaker"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function removeTab() { + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js b/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js new file mode 100644 index 0000000000..37bde1a275 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js @@ -0,0 +1,145 @@ +"use strict"; + +/* 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 PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +let sitePermissionsDialog; + +let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("http://www.example.com"), + {} +); +let pbPrincipal = Services.scriptSecurityManager.principalWithOA(principal, { + privateBrowsingId: 1, +}); +let principalB = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://example.org"), + {} +); + +/** + * Replaces the default permissions defined in browser/app/permissions with our + * own test-only permissions and instructs the permission manager to import + * them. + */ +async function addDefaultTestPermissions() { + // create a file in the temp directory with the defaults. + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("test_default_permissions"); + + await IOUtils.writeUTF8( + file.path, + `origin\tinstall\t1\t${principal.origin}\norigin\tinstall\t1\t${pbPrincipal.origin}\n` + ); + + // Change the default permission file path. + await SpecialPowers.pushPrefEnv({ + set: [ + ["permissions.manager.defaultsUrl", Services.io.newFileURI(file).spec], + ], + }); + + // Call the permission manager to reload default permissions from file. + Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); + + registerCleanupFunction(async () => { + // Clean up temporary default permission file. + await IOUtils.remove(file.path); + + // Restore non-test default permissions. + await SpecialPowers.popPrefEnv(); + Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); + }); +} + +async function openPermissionsDialog() { + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let settingsButton = doc.getElementById("addonExceptions"); + settingsButton.click(); + }); + + sitePermissionsDialog = await dialogOpened; + await sitePermissionsDialog.document.mozSubdialogReady; +} + +add_setup(async function () { + await addDefaultTestPermissions(); +}); + +/** + * Tests that default (persistent) private browsing permissions can be removed. + */ +add_task(async function removeAll() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openPermissionsDialog(); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + // First item in the richlistbox contains column headers. + Assert.equal( + richlistbox.itemCount, + 2, + "Should have the two default permission entries initially." + ); + + info("Adding a new non-default install permission"); + PermissionTestUtils.add(principalB, "install", Services.perms.ALLOW_ACTION); + + info("Waiting for the permission to appear in the list."); + await BrowserTestUtils.waitForMutationCondition( + richlistbox, + { childList: true }, + () => richlistbox.itemCount == 3 + ); + + info("Clicking remove all."); + doc.getElementById("removeAllPermissions").click(); + + info("Waiting for all list items to be cleared."); + await BrowserTestUtils.waitForMutationCondition( + richlistbox, + { childList: true }, + () => richlistbox.itemCount == 0 + ); + + let dialogClosePromise = BrowserTestUtils.waitForEvent( + sitePermissionsDialog, + "dialogclosing", + true + ); + + info("Accepting dialog to apply the changes."); + doc.querySelector("dialog").getButton("accept").click(); + + info("Waiting for dialog to close."); + await dialogClosePromise; + + info("Waiting for all permissions to be removed."); + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(principal, "install") == null && + PermissionTestUtils.getPermissionObject(pbPrincipal, "install") == null && + PermissionTestUtils.getPermissionObject(principalB, "install") == null + ); + + info("Opening the permissions dialog again."); + await openPermissionsDialog(); + + Assert.equal( + richlistbox.itemCount, + 0, + "Permission list should still be empty." + ); + + // Cleanup + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Services.perms.removeAll(); +}); diff --git a/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js b/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js new file mode 100644 index 0000000000..537ee3db72 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js @@ -0,0 +1,38 @@ +"use strict"; + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +add_task(async function urlFieldVisibleForPopupPermissions(finish) { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + let popupPolicyCheckbox = doc.getElementById("popupPolicy"); + ok( + !popupPolicyCheckbox.checked, + "popupPolicyCheckbox should be unchecked by default" + ); + popupPolicyCheckbox.click(); + let popupPolicyButton = doc.getElementById("popupPolicyButton"); + ok(popupPolicyButton, "popupPolicyButton found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + popupPolicyButton.click(); + let dialog = await dialogPromise; + ok(dialog, "dialog loaded"); + + let urlLabel = dialog.document.getElementById("urlLabel"); + ok( + !urlLabel.hidden, + "urlLabel should be visible when one of block/session/allow visible" + ); + let url = dialog.document.getElementById("url"); + ok( + !url.hidden, + "url should be visible when one of block/session/allow visible" + ); + + popupPolicyCheckbox.click(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_primaryPassword.js b/browser/components/preferences/tests/browser_primaryPassword.js new file mode 100644 index 0000000000..4de28a1fdb --- /dev/null +++ b/browser/components/preferences/tests/browser_primaryPassword.js @@ -0,0 +1,130 @@ +const { OSKeyStoreTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/OSKeyStoreTestUtils.sys.mjs" +); +const { OSKeyStore } = ChromeUtils.importESModule( + "resource://gre/modules/OSKeyStore.sys.mjs" +); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected"); + + let doc = gBrowser.contentDocument; + // Fake the subdialog and LoginHelper + let win = doc.defaultView; + let dialogURL = ""; + let dialogOpened = false; + XPCOMUtils.defineLazyGetter(win, "gSubDialog", () => ({ + open(aDialogURL, { closingCallback: aCallback }) { + dialogOpened = true; + dialogURL = aDialogURL; + primaryPasswordSet = primaryPasswordNextState; + aCallback(); + }, + })); + + let primaryPasswordSet = false; + win.LoginHelper = { + isPrimaryPasswordSet() { + return primaryPasswordSet; + }, + }; + + let checkbox = doc.querySelector("#useMasterPassword"); + checkbox.scrollIntoView(); + ok( + !checkbox.checked, + "primary password checkbox should be unchecked by default" + ); + let button = doc.getElementById("changeMasterPassword"); + ok(button.disabled, "primary password button should be disabled by default"); + + let primaryPasswordNextState = false; + if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin() && OSKeyStore.canReauth()) { + let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); + checkbox.click(); + info("waiting for os auth dialog to appear and get canceled"); + await osAuthDialogShown; + await TestUtils.waitForCondition( + () => !checkbox.checked, + "wait for checkbox to get unchecked" + ); + ok(!dialogOpened, "the dialog should not have opened"); + ok( + !dialogURL, + "the changemp dialog should not have been opened when the os auth dialog is canceled" + ); + ok( + !checkbox.checked, + "primary password checkbox should be unchecked after canceling os auth dialog" + ); + ok(button.disabled, "button should be disabled after canceling os auth"); + } + + primaryPasswordNextState = true; + if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin() && OSKeyStore.canReauth()) { + let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + checkbox.click(); + info("waiting for os auth dialog to appear"); + await osAuthDialogShown; + info("waiting for dialogURL to get set"); + await TestUtils.waitForCondition( + () => dialogURL, + "wait for open to get called asynchronously" + ); + is( + dialogURL, + "chrome://mozapps/content/preferences/changemp.xhtml", + "clicking on the checkbox should open the primary password dialog" + ); + } else { + primaryPasswordSet = true; + doc.defaultView.gPrivacyPane._initMasterPasswordUI(); + await TestUtils.waitForCondition( + () => !button.disabled, + "waiting for primary password button to get enabled" + ); + } + ok(!button.disabled, "primary password button should now be enabled"); + ok(checkbox.checked, "primary password checkbox should be checked now"); + + dialogURL = ""; + button.doCommand(); + await TestUtils.waitForCondition( + () => dialogURL, + "wait for open to get called asynchronously" + ); + is( + dialogURL, + "chrome://mozapps/content/preferences/changemp.xhtml", + "clicking on the button should open the primary password dialog" + ); + ok(!button.disabled, "primary password button should still be enabled"); + ok(checkbox.checked, "primary password checkbox should be checked still"); + + // Confirm that we won't automatically respond to the dialog, + // since we don't expect a dialog here, we want the test to fail if one appears. + is( + Services.prefs.getStringPref( + "toolkit.osKeyStore.unofficialBuildOnlyLogin", + "" + ), + "", + "Pref should be set to an empty string" + ); + + primaryPasswordNextState = false; + dialogURL = ""; + checkbox.click(); + is( + dialogURL, + "chrome://mozapps/content/preferences/removemp.xhtml", + "clicking on the checkbox to uncheck primary password should show the removal dialog" + ); + ok(button.disabled, "primary password button should now be disabled"); + ok(!checkbox.checked, "primary password checkbox should now be unchecked"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js b/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js new file mode 100644 index 0000000000..722fe9a215 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests the Privacy pane's Cookie Banner Handling UI. + +"use strict"; + +const FEATURE_PREF = "cookiebanners.ui.desktop.enabled"; +const MODE_PREF = "cookiebanners.service.mode"; +const PBM_MODE_PREF = "cookiebanners.service.mode.privateBrowsing"; +const DETECT_ONLY_PREF = "cookiebanners.service.detectOnly"; + +const GROUPBOX_ID = "cookieBannerHandlingGroup"; +const CHECKBOX_ID = "handleCookieBanners"; + +// Test the section is hidden on page load if the feature pref is disabled. +add_task(async function test_section_hidden_when_feature_flag_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, false], + [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let groupbox = browser.contentDocument.getElementById(GROUPBOX_ID); + is_element_hidden(groupbox, "#cookieBannerHandlingGroup is hidden"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the section is shown on page load if the feature pref is enabled. +add_task(async function test_section_shown_when_feature_flag_enabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let groupbox = browser.contentDocument.getElementById(GROUPBOX_ID); + is_element_visible(groupbox, "#cookieBannerHandlingGroup is visible"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is unchecked in DISABLED mode. +add_task(async function test_checkbox_unchecked_disabled_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(!checkbox.checked, "checkbox is not checked in DISABLED mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is unchecked in detect-only mode. +add_task(async function test_checkbox_unchecked_detect_only_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT], + [DETECT_ONLY_PREF, true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(!checkbox.checked, "checkbox is not checked in detect-only mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is checked in REJECT_OR_ACCEPT mode. +add_task(async function test_checkbox_checked_reject_or_accept_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(checkbox.checked, "checkbox is checked in REJECT_OR_ACCEPT mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is checked in REJECT mode. +add_task(async function test_checkbox_checked_reject_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(checkbox.checked, "checkbox is checked in REJECT mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test that toggling the checkbox toggles the mode pref value as expected, +// and also disables detect only mode, as expected. +add_task(async function test_checkbox_modifies_prefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_UNSET], + [PBM_MODE_PREF, Ci.nsICookieBannerService.MODE_UNSET], + [DETECT_ONLY_PREF, true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkboxSelector = "#" + CHECKBOX_ID; + let checkbox = browser.contentDocument.querySelector(checkboxSelector); + let section = browser.contentDocument.getElementById(GROUPBOX_ID); + + section.scrollIntoView(); + + Assert.ok( + !checkbox.checked, + "initially, the checkbox should be unchecked" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + checkboxSelector, + {}, + browser + ); + Assert.ok(checkbox.checked, "checkbox should be checked"); + Assert.equal( + Ci.nsICookieBannerService.MODE_REJECT, + Services.prefs.getIntPref(MODE_PREF), + "cookie banner handling mode should be set to REJECT mode after checking the checkbox" + ); + Assert.equal( + Ci.nsICookieBannerService.MODE_REJECT, + Services.prefs.getIntPref(PBM_MODE_PREF), + "cookie banner handling mode for PBM should be set to REJECT mode after checking the checkbox" + ); + Assert.equal( + false, + Services.prefs.getBoolPref(DETECT_ONLY_PREF), + "cookie banner handling detect-only mode should be disabled after checking the checkbox" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + checkboxSelector, + {}, + browser + ); + Assert.ok(!checkbox.checked, "checkbox should be unchecked"); + Assert.equal( + Ci.nsICookieBannerService.MODE_DISABLED, + Services.prefs.getIntPref(MODE_PREF), + "cookie banner handling mode should be set to DISABLED mode after unchecking the checkbox" + ); + Assert.equal( + Ci.nsICookieBannerService.MODE_DISABLED, + Services.prefs.getIntPref(PBM_MODE_PREF), + "cookie banner handling mode for PBM should be set to DISABLED mode after unchecking the checkbox" + ); + Assert.equal( + false, + Services.prefs.getBoolPref(DETECT_ONLY_PREF), + "cookie banner handling detect-only mode should still be disabled after unchecking the checkbox" + ); + } + ); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js new file mode 100644 index 0000000000..48469cfce4 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js @@ -0,0 +1,844 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(4); + +const { EnterprisePolicyTesting, PoliciesPrefTracker } = + ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); + +ChromeUtils.defineESModuleGetters(this, { + DoHConfigController: "resource:///modules/DoHConfig.sys.mjs", + DoHController: "resource:///modules/DoHController.sys.mjs", + DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs", +}); + +const TRR_MODE_PREF = "network.trr.mode"; +const TRR_URI_PREF = "network.trr.uri"; +const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri"; +const ROLLOUT_ENABLED_PREF = "doh-rollout.enabled"; +const ROLLOUT_SELF_ENABLED_PREF = "doh-rollout.self-enabled"; +const HEURISTICS_DISABLED_PREF = "doh-rollout.disable-heuristics"; +const FIRST_RESOLVER_VALUE = DoHTestUtils.providers[0].uri; +const SECOND_RESOLVER_VALUE = DoHTestUtils.providers[1].uri; +const DEFAULT_RESOLVER_VALUE = FIRST_RESOLVER_VALUE; + +const defaultPrefValues = Object.freeze({ + [TRR_MODE_PREF]: 0, + [TRR_CUSTOM_URI_PREF]: "", +}); + +// See bug 1741554. This test should not actually try to create a connection to +// the real DoH endpoint. But a background request could do that while the test +// is in progress, before we've actually disabled TRR, and would cause a crash +// due to connecting to a non-local IP. +// To prevent that we override the IP to a local address. +Cc["@mozilla.org/network/native-dns-override;1"] + .getService(Ci.nsINativeDNSResolverOverride) + .addIPOverride("mozilla.cloudflare-dns.com", "127.0.0.1"); + +async function clearEvents() { + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_ALL_CHANNELS, + true + ).parent; + return !events || !events.length; + }); +} + +async function getEvent(filter1, filter2) { + let event = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_ALL_CHANNELS, + true + ).parent; + return events?.find(e => e[1] == filter1 && e[2] == filter2); + }, "recorded telemetry for the load"); + event.shift(); + return event; +} + +async function resetPrefs() { + await DoHTestUtils.resetRemoteSettingsConfig(); + await DoHController._uninit(); + Services.prefs.clearUserPref(TRR_MODE_PREF); + Services.prefs.clearUserPref(TRR_URI_PREF); + Services.prefs.clearUserPref(TRR_CUSTOM_URI_PREF); + Services.prefs.getChildList("doh-rollout.").forEach(pref => { + Services.prefs.clearUserPref(pref); + }); + // Clear out any telemetry events generated by DoHController so that we don't + // confuse tests running after this one that are looking at those. + Services.telemetry.clearEvents(); + await DoHController.init(); +} +Services.prefs.setStringPref("network.trr.confirmationNS", "skip"); + +registerCleanupFunction(async () => { + await resetPrefs(); + Services.prefs.clearUserPref("network.trr.confirmationNS"); +}); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["toolkit.telemetry.testing.overrideProductsCheck", true]], + }); + + await DoHTestUtils.resetRemoteSettingsConfig(); +}); + +function waitForPrefObserver(name) { + return new Promise(resolve => { + const observer = { + observe(aSubject, aTopic, aData) { + if (aData == name) { + Services.prefs.removeObserver(name, observer); + resolve(); + } + }, + }; + Services.prefs.addObserver(name, observer); + }); +} + +async function testWithProperties(props, startTime) { + info( + Date.now() - + startTime + + ": testWithProperties: testing with " + + JSON.stringify(props) + ); + + // There are two different signals that the DoHController is ready, depending + // on the config being tested. If we're setting the TRR mode pref, we should + // expect the disable-heuristics pref to be set as the signal. Else, we can + // expect the self-enabled pref as the signal. + let rolloutReadyPromise; + if (props.hasOwnProperty(TRR_MODE_PREF)) { + if ( + [2, 3, 5].includes(props[TRR_MODE_PREF]) && + props.hasOwnProperty(ROLLOUT_ENABLED_PREF) + ) { + // Only initialize the promise if we're going to enable the rollout - + // otherwise we will never await it, which could cause a leak if it doesn't + // end up resolving. + rolloutReadyPromise = waitForPrefObserver(HEURISTICS_DISABLED_PREF); + } + Services.prefs.setIntPref(TRR_MODE_PREF, props[TRR_MODE_PREF]); + } + if (props.hasOwnProperty(ROLLOUT_ENABLED_PREF)) { + if (!rolloutReadyPromise) { + rolloutReadyPromise = waitForPrefObserver(ROLLOUT_SELF_ENABLED_PREF); + } + Services.prefs.setBoolPref( + ROLLOUT_ENABLED_PREF, + props[ROLLOUT_ENABLED_PREF] + ); + await rolloutReadyPromise; + } + if (props.hasOwnProperty(TRR_CUSTOM_URI_PREF)) { + Services.prefs.setStringPref( + TRR_CUSTOM_URI_PREF, + props[TRR_CUSTOM_URI_PREF] + ); + } + if (props.hasOwnProperty(TRR_URI_PREF)) { + Services.prefs.setStringPref(TRR_URI_PREF, props[TRR_URI_PREF]); + } + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + info(Date.now() - startTime + ": testWithProperties: tab now open"); + let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup"); + let uriTextbox = doc.getElementById("dohEnabledInputField"); + let resolverMenulist = doc.getElementById("dohStrictResolverChoices"); + let modePrefChangedPromise; + let uriPrefChangedPromise; + let disableHeuristicsPrefChangedPromise; + + modeRadioGroup.scrollIntoView(); + + if (props.hasOwnProperty("expectedSelectedIndex")) { + await TestUtils.waitForCondition( + () => modeRadioGroup.selectedIndex === props.expectedSelectedIndex + ); + is( + modeRadioGroup.selectedIndex, + props.expectedSelectedIndex, + "dohCategoryRadioGroup has expected selected index" + ); + } + if (props.hasOwnProperty("expectedUriValue")) { + await TestUtils.waitForCondition( + () => uriTextbox.value === props.expectedUriValue + ); + is( + uriTextbox.value, + props.expectedUriValue, + "URI textbox has expected value" + ); + } + if (props.hasOwnProperty("expectedResolverListValue")) { + await TestUtils.waitForCondition( + () => resolverMenulist.value === props.expectedResolverListValue + ); + is( + resolverMenulist.value, + props.expectedResolverListValue, + "resolver menulist has expected value" + ); + } + + if (props.clickMode) { + await clearEvents(); + info( + Date.now() - + startTime + + ": testWithProperties: clickMode, waiting for the pref observer" + ); + modePrefChangedPromise = waitForPrefObserver(TRR_MODE_PREF); + if (props.hasOwnProperty("expectedDisabledHeuristics")) { + disableHeuristicsPrefChangedPromise = waitForPrefObserver( + HEURISTICS_DISABLED_PREF + ); + } + info( + Date.now() - startTime + ": testWithProperties: clickMode, pref changed" + ); + let option = doc.getElementById(props.clickMode); + option.scrollIntoView(); + let win = doc.ownerGlobal; + EventUtils.synthesizeMouseAtCenter(option, {}, win); + info( + `${Date.now() - startTime} : testWithProperties: clickMode=${ + props.clickMode + }, mouse click synthesized` + ); + let clickEvent = await getEvent("security.doh.settings", "mode_changed"); + Assert.deepEqual(clickEvent, [ + "security.doh.settings", + "mode_changed", + "button", + props.clickMode, + ]); + } + if (props.hasOwnProperty("selectResolver")) { + await clearEvents(); + info( + Date.now() - + startTime + + ": testWithProperties: selectResolver, creating change event" + ); + resolverMenulist.focus(); + resolverMenulist.value = props.selectResolver; + resolverMenulist.dispatchEvent(new Event("input", { bubbles: true })); + resolverMenulist.dispatchEvent(new Event("command", { bubbles: true })); + info( + Date.now() - + startTime + + ": testWithProperties: selectResolver, item value set and events dispatched" + ); + let choiceEvent = await getEvent( + "security.doh.settings", + "provider_choice" + ); + Assert.deepEqual(choiceEvent, [ + "security.doh.settings", + "provider_choice", + "value", + props.selectResolver, + ]); + } + if (props.hasOwnProperty("inputUriKeys")) { + info( + Date.now() - + startTime + + ": testWithProperties: inputUriKeys, waiting for the pref observer" + ); + uriPrefChangedPromise = waitForPrefObserver(TRR_CUSTOM_URI_PREF); + info( + Date.now() - + startTime + + ": testWithProperties: inputUriKeys, pref changed, now enter the new value" + ); + let win = doc.ownerGlobal; + uriTextbox.focus(); + uriTextbox.value = props.inputUriKeys; + uriTextbox.dispatchEvent(new win.Event("input", { bubbles: true })); + uriTextbox.dispatchEvent(new win.Event("change", { bubbles: true })); + info( + Date.now() - + startTime + + ": testWithProperties: inputUriKeys, input and change events dispatched" + ); + } + + info( + Date.now() - + startTime + + ": testWithProperties: waiting for any of uri and mode prefs to change" + ); + await Promise.all([ + uriPrefChangedPromise, + modePrefChangedPromise, + disableHeuristicsPrefChangedPromise, + ]); + info(Date.now() - startTime + ": testWithProperties: prefs changed"); + + if (props.hasOwnProperty("expectedFinalUriPref")) { + if (props.expectedFinalUriPref) { + let uriPref = Services.prefs.getStringPref(TRR_URI_PREF); + is( + uriPref, + props.expectedFinalUriPref, + "uri pref ended up with the expected value" + ); + } else { + ok( + !Services.prefs.prefHasUserValue(TRR_URI_PREF), + `uri pref ended up with the expected value (unset) got ${Services.prefs.getStringPref( + TRR_URI_PREF + )}` + ); + } + } + + if (props.hasOwnProperty("expectedModePref")) { + let modePref = Services.prefs.getIntPref(TRR_MODE_PREF); + is( + modePref, + props.expectedModePref, + "mode pref ended up with the expected value" + ); + } + + if (props.hasOwnProperty("expectedDisabledHeuristics")) { + let disabledHeuristicsPref = Services.prefs.getBoolPref( + HEURISTICS_DISABLED_PREF + ); + is( + disabledHeuristicsPref, + props.expectedDisabledHeuristics, + "disable-heuristics pref ended up with the expected value" + ); + } + + if (props.hasOwnProperty("expectedFinalCustomUriPref")) { + let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF); + is( + customUriPref, + props.expectedFinalCustomUriPref, + "custom_uri pref ended up with the expected value" + ); + } + + if (props.hasOwnProperty("expectedModeValue")) { + let modeValue = Services.prefs.getIntPref(TRR_MODE_PREF); + is(modeValue, props.expectedModeValue, "mode pref has expected value"); + } + + gBrowser.removeCurrentTab(); + info(Date.now() - startTime + ": testWithProperties: fin"); +} + +add_task(async function default_values() { + let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF); + let uriPrefHasUserValue = Services.prefs.prefHasUserValue(TRR_URI_PREF); + let modePref = Services.prefs.getIntPref(TRR_MODE_PREF); + is( + modePref, + defaultPrefValues[TRR_MODE_PREF], + `Actual value of ${TRR_MODE_PREF} matches expected default value` + ); + ok( + !uriPrefHasUserValue, + `Actual value of ${TRR_URI_PREF} matches expected default value (unset)` + ); + is( + customUriPref, + defaultPrefValues[TRR_CUSTOM_URI_PREF], + `Actual value of ${TRR_CUSTOM_URI_PREF} matches expected default value` + ); +}); + +const DEFAULT_OPTION_INDEX = 0; +const ENABLED_OPTION_INDEX = 1; +const STRICT_OPTION_INDEX = 2; +const OFF_OPTION_INDEX = 3; + +let testVariations = [ + // verify state with defaults + { + name: "default", + expectedModePref: 0, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + expectedUriValue: "", + }, + + // verify each of the modes maps to the correct checked state + { + name: "mode 0", + [TRR_MODE_PREF]: 0, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + }, + { + name: "mode 1", + [TRR_MODE_PREF]: 1, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + { + name: "mode 2", + [TRR_MODE_PREF]: 2, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + expectedFinalUriPref: "", + }, + { + name: "mode 3", + [TRR_MODE_PREF]: 3, + expectedSelectedIndex: STRICT_OPTION_INDEX, + expectedFinalUriPref: "", + }, + { + name: "mode 4", + [TRR_MODE_PREF]: 4, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + { + name: "mode 5", + [TRR_MODE_PREF]: 5, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + // verify an out of bounds mode value maps to the correct checked state + { + name: "mode out-of-bounds", + [TRR_MODE_PREF]: 77, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + + // verify automatic heuristics states + { + name: "heuristics on and mode unset", + [TRR_MODE_PREF]: 0, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + }, + { + name: "heuristics on and mode set to 2", + [TRR_MODE_PREF]: 2, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + }, + { + name: "heuristics on but disabled, mode unset", + [TRR_MODE_PREF]: 5, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + { + name: "heuristics on but disabled, mode set to 2", + [TRR_MODE_PREF]: 2, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + }, + + // verify picking each radio button option gives the right outcomes + { + name: "toggle mode on", + clickMode: "dohEnabledRadio", + expectedModeValue: 2, + expectedUriValue: "", + expectedFinalUriPref: "", + }, + { + name: "toggle mode off", + [TRR_MODE_PREF]: 2, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + clickMode: "dohOffRadio", + expectedModePref: 5, + }, + { + name: "toggle mode off when on due to heuristics", + [TRR_MODE_PREF]: 0, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + clickMode: "dohOffRadio", + expectedModePref: 5, + expectedDisabledHeuristics: true, + }, + // Test selecting non-default, non-custom TRR provider, NextDNS. + { + name: "Select NextDNS as TRR provider", + [TRR_MODE_PREF]: 2, + selectResolver: SECOND_RESOLVER_VALUE, + expectedFinalUriPref: SECOND_RESOLVER_VALUE, + }, + // Test selecting non-default, non-custom TRR provider, NextDNS, + // with DoH not enabled. The provider selection should stick. + { + name: "Select NextDNS as TRR provider in mode 0", + [TRR_MODE_PREF]: 0, + selectResolver: SECOND_RESOLVER_VALUE, + expectedFinalUriPref: SECOND_RESOLVER_VALUE, + }, + { + name: "return to default from NextDNS", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: SECOND_RESOLVER_VALUE, + expectedResolverListValue: SECOND_RESOLVER_VALUE, + selectResolver: DEFAULT_RESOLVER_VALUE, + expectedFinalUriPref: FIRST_RESOLVER_VALUE, + }, + // test that selecting Custom, when we have a TRR_CUSTOM_URI_PREF subsequently changes TRR_URI_PREF + { + name: "select custom with existing custom_uri pref value", + [TRR_MODE_PREF]: 2, + [TRR_CUSTOM_URI_PREF]: "https://example.com", + expectedModeValue: 2, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + selectResolver: "custom", + expectedUriValue: "https://example.com", + expectedFinalUriPref: "https://example.com", + expectedFinalCustomUriPref: "https://example.com", + }, + { + name: "select custom and enter new custom_uri pref value", + [TRR_URI_PREF]: "", + [TRR_CUSTOM_URI_PREF]: "", + clickMode: "dohEnabledRadio", + selectResolver: "custom", + inputUriKeys: "https://custom.com", + expectedModePref: 2, + expectedFinalUriPref: "https://custom.com", + expectedFinalCustomUriPref: "https://custom.com", + }, + + { + name: "return to default from custom", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "https://custom.com", + expectedUriValue: "https://example.com", + expectedResolverListValue: "custom", + selectResolver: DEFAULT_RESOLVER_VALUE, + expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, + expectedFinalCustomUriPref: "https://example.com", + }, + { + name: "clear the custom uri", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "https://example.com", + expectedUriValue: "https://example.com", + expectedResolverListValue: "custom", + inputUriKeys: "", + expectedFinalUriPref: " ", + expectedFinalCustomUriPref: "", + }, + { + name: "empty default resolver list", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "", + expectedUriValue: "https://example.com", + expectedResolverListValue: "custom", + expectedFinalUriPref: "https://example.com", + expectedFinalCustomUriPref: "https://example.com", + }, +]; + +for (let props of testVariations) { + add_task(async function testVariation() { + let startTime = Date.now(); + info("starting test: " + props.name); + await testWithProperties(props, startTime); + await resetPrefs(); + }); +} + +add_task(async function testRemoteSettingsEnable() { + let startTime = Date.now(); + // Enable the rollout. + await DoHTestUtils.loadRemoteSettingsConfig({ + providers: "example-1, example-2", + rolloutEnabled: true, + steeringEnabled: false, + steeringProviders: "", + autoDefaultEnabled: false, + autoDefaultProviders: "", + id: "global", + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + info(Date.now() - startTime + ": testWithProperties: tab now open"); + let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup"); + + is(modeRadioGroup.value, "0", "expecting default mode"); + + let status = doc.getElementById("dohStatus"); + await TestUtils.waitForCondition( + () => document.l10n.getAttributes(status).args.status == "Active" + ); + is( + document.l10n.getAttributes(status).args.status, + "Active", + "expecting status active" + ); + + let provider = doc.getElementById("dohResolver"); + is( + provider.hidden, + false, + "Provider should not be hidden when status is active" + ); + await TestUtils.waitForCondition( + () => + document.l10n.getAttributes(provider).args.name == + DoHConfigController.currentConfig.providerList[0].UIName + ); + is( + document.l10n.getAttributes(provider).args.name, + DoHConfigController.currentConfig.providerList[0].UIName, + "expecting the right provider name" + ); + + let option = doc.getElementById("dohEnabledRadio"); + option.scrollIntoView(); + let win = doc.ownerGlobal; + EventUtils.synthesizeMouseAtCenter(option, {}, win); + + await TestUtils.waitForCondition(() => + Services.prefs.prefHasUserValue("doh-rollout.disable-heuristics") + ); + is(provider.hidden, false); + await TestUtils.waitForCondition( + () => + document.l10n.getAttributes(provider).args.name == + DoHConfigController.currentConfig.providerList[0].UIName + ); + is( + document.l10n.getAttributes(provider).args.name, + DoHConfigController.currentConfig.providerList[0].UIName, + "expecting the right provider name" + ); + is( + Services.prefs.getIntPref("network.trr.mode"), + Ci.nsIDNSService.MODE_TRRFIRST + ); + + option = doc.getElementById("dohOffRadio"); + option.scrollIntoView(); + win = doc.ownerGlobal; + EventUtils.synthesizeMouseAtCenter(option, {}, win); + await TestUtils.waitForCondition(() => status.innerHTML == "Status: Off"); + is( + Services.prefs.getIntPref("network.trr.mode"), + Ci.nsIDNSService.MODE_TRROFF + ); + is(provider.hidden, true, "Expecting provider to be hidden when DoH is off"); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function testEnterprisePolicy() { + async function withPolicy(policy, fn, preFn = () => {}) { + await resetPrefs(); + PoliciesPrefTracker.start(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy); + await preFn(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup"); + let resolverMenulist = doc.getElementById("dohEnabledResolverChoices"); + let uriTextbox = doc.getElementById("dohEnabledInputField"); + + await fn({ + modeRadioGroup, + resolverMenulist, + doc, + uriTextbox, + }); + + gBrowser.removeCurrentTab(); + EnterprisePolicyTesting.resetRunOnceState(); + PoliciesPrefTracker.stop(); + } + + info("Check that a locked policy does not allow any changes in the UI"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: true, + ProviderURL: "https://examplelocked.com/provider", + ExcludedDomains: ["examplelocked.com", "example.org"], + Locked: true, + }, + }, + }, + async res => { + is(res.modeRadioGroup.disabled, true, "The mode menu should be locked."); + is(res.modeRadioGroup.value, "2", "Should be enabled"); + is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); + is( + res.uriTextbox.value, + "https://examplelocked.com/provider", + "Custom URI should be set" + ); + } + ); + + info("Check that an unlocked policy has editable fields in the dialog"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: true, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + }, + }, + }, + async res => { + is( + res.modeRadioGroup.disabled, + false, + "The mode menu should not be locked." + ); + is(res.modeRadioGroup.value, "2", "Should be enabled"); + is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); + is( + res.uriTextbox.value, + "https://example.com/provider", + "Expected custom resolver" + ); + } + ); + + info("Check that a locked disabled policy disables the buttons"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: false, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + Locked: true, + }, + }, + }, + async res => { + is(res.modeRadioGroup.disabled, true, "The mode menu should be locked."); + is(res.modeRadioGroup.value, "5", "Should be disabled"); + } + ); + + info("Check that an unlocked disabled policy has editable fields"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: false, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + }, + }, + }, + async res => { + is( + res.modeRadioGroup.disabled, + false, + "The mode menu should not be locked." + ); + is(res.modeRadioGroup.value, "5", "Should be disabled"); + } + ); + + info("Check that the remote settings config doesn't override the policy"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: true, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + }, + }, + }, + async res => { + is( + res.modeRadioGroup.disabled, + false, + "The mode menu should not be locked." + ); + is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); + is( + res.uriTextbox.value, + "https://example.com/provider", + "Expected custom resolver" + ); + }, + async function runAfterSettingPolicy() { + await DoHTestUtils.loadRemoteSettingsConfig({ + providers: "example-1, example-2", + rolloutEnabled: true, + steeringEnabled: false, + steeringProviders: "", + autoDefaultEnabled: false, + autoDefaultProviders: "", + id: "global", + }); + } + ); +}); + +add_task(async function clickWarnButton() { + Services.prefs.setBoolPref( + "network.trr_ui.show_fallback_warning_option", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + await clearEvents(); + let checkbox = doc.getElementById("dohWarnCheckbox1"); + checkbox.click(); + + let event = await getEvent("security.doh.settings", "warn_checkbox"); + Assert.deepEqual(event, [ + "security.doh.settings", + "warn_checkbox", + "checkbox", + "true", + ]); + Assert.equal( + Services.prefs.getBoolPref("network.trr.display_fallback_warning"), + true, + "Clicking the checkbox should change the pref" + ); + + checkbox.click(); + event = await getEvent("security.doh.settings", "warn_checkbox"); + Assert.deepEqual(event, [ + "security.doh.settings", + "warn_checkbox", + "checkbox", + "false", + ]); + Assert.equal( + Services.prefs.getBoolPref("network.trr.display_fallback_warning"), + false, + "Clicking the checkbox should change the pref" + ); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js b/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js new file mode 100644 index 0000000000..883e19acc2 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js @@ -0,0 +1,855 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This tests the Privacy pane's Firefox Suggest UI. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +const CONTAINER_ID = "firefoxSuggestContainer"; +const NONSPONSORED_TOGGLE_ID = "firefoxSuggestNonsponsoredToggle"; +const SPONSORED_TOGGLE_ID = "firefoxSuggestSponsoredToggle"; +const DATA_COLLECTION_TOGGLE_ID = "firefoxSuggestDataCollectionToggle"; +const INFO_BOX_ID = "firefoxSuggestInfoBox"; +const INFO_TEXT_ID = "firefoxSuggestInfoText"; +const LEARN_MORE_CLASS = "firefoxSuggestLearnMore"; +const BEST_MATCH_CONTAINER_ID = "firefoxSuggestBestMatchContainer"; +const BEST_MATCH_CHECKBOX_ID = "firefoxSuggestBestMatch"; +const BUTTON_RESTORE_DISMISSED_ID = "restoreDismissedSuggestions"; +const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST = + "browser.urlbar.quicksuggest.blockedDigests"; +const PREF_URLBAR_WEATHER_USER_ENABLED = "browser.urlbar.suggest.weather"; + +// Maps text element IDs to `{ enabled, disabled }`, where `enabled` is the +// expected l10n ID when the Firefox Suggest feature is enabled, and `disabled` +// is when disabled. +const EXPECTED_L10N_IDS = { + locationBarGroupHeader: { + enabled: "addressbar-header-firefox-suggest", + disabled: "addressbar-header", + }, + locationBarSuggestionLabel: { + enabled: "addressbar-suggest-firefox-suggest", + disabled: "addressbar-suggest", + }, +}; + +// This test can take a while due to the many permutations some of these tasks +// run through, so request a longer timeout. +requestLongerTimeout(10); + +// The following tasks check the visibility of the Firefox Suggest UI based on +// the value of the feature pref. See doVisibilityTest(). + +add_task(async function historyToOffline() { + await doVisibilityTest({ + initialScenario: "history", + initialExpectedVisibility: false, + newScenario: "offline", + newExpectedVisibility: true, + }); +}); + +add_task(async function historyToOnline() { + await doVisibilityTest({ + initialScenario: "history", + initialExpectedVisibility: false, + newScenario: "online", + newExpectedVisibility: true, + }); +}); + +add_task(async function offlineToHistory() { + await doVisibilityTest({ + initialScenario: "offline", + initialExpectedVisibility: true, + newScenario: "history", + newExpectedVisibility: false, + }); +}); + +add_task(async function offlineToOnline() { + await doVisibilityTest({ + initialScenario: "offline", + initialExpectedVisibility: true, + newScenario: "online", + newExpectedVisibility: true, + }); +}); + +add_task(async function onlineToHistory() { + await doVisibilityTest({ + initialScenario: "online", + initialExpectedVisibility: true, + newScenario: "history", + newExpectedVisibility: false, + }); +}); + +add_task(async function onlineToOffline() { + await doVisibilityTest({ + initialScenario: "online", + initialExpectedVisibility: true, + newScenario: "offline", + newExpectedVisibility: true, + }); +}); + +/** + * Runs a test that checks the visibility of the Firefox Suggest preferences UI + * based on scenario pref. + * + * @param {string} initialScenario + * The initial scenario. + * @param {boolean} initialExpectedVisibility + * Whether the UI should be visible with the initial scenario. + * @param {string} newScenario + * The updated scenario. + * @param {boolean} newExpectedVisibility + * Whether the UI should be visible after setting the new scenario. + */ +async function doVisibilityTest({ + initialScenario, + initialExpectedVisibility, + newScenario, + newExpectedVisibility, +}) { + info( + "Running visibility test: " + + JSON.stringify( + { + initialScenario, + initialExpectedVisibility, + newScenario, + newExpectedVisibility, + }, + null, + 2 + ) + ); + + // Set the initial scenario. + await QuickSuggestTestUtils.setScenario(initialScenario); + + Assert.equal( + Services.prefs.getBoolPref("browser.urlbar.quicksuggest.enabled"), + initialExpectedVisibility, + `quicksuggest.enabled is correct after setting initial scenario, initialExpectedVisibility=${initialExpectedVisibility}` + ); + + // Open prefs and check the initial visibility. + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CONTAINER_ID); + Assert.equal( + BrowserTestUtils.is_visible(container), + initialExpectedVisibility, + `The container has the expected initial visibility, initialExpectedVisibility=${initialExpectedVisibility}` + ); + + // Check the text elements' l10n IDs. + for (let [id, { enabled, disabled }] of Object.entries(EXPECTED_L10N_IDS)) { + Assert.equal( + doc.getElementById(id).dataset.l10nId, + initialExpectedVisibility ? enabled : disabled, + `Initial l10n ID for element with ID ${id}, initialExpectedVisibility=${initialExpectedVisibility}` + ); + } + + // Set the new scenario. + await QuickSuggestTestUtils.setScenario(newScenario); + + Assert.equal( + Services.prefs.getBoolPref("browser.urlbar.quicksuggest.enabled"), + newExpectedVisibility, + `quicksuggest.enabled is correct after setting new scenario, newExpectedVisibility=${newExpectedVisibility}` + ); + + // Check visibility again. + Assert.equal( + BrowserTestUtils.is_visible(container), + newExpectedVisibility, + `The container has the expected visibility after setting new scenario, newExpectedVisibility=${newExpectedVisibility}` + ); + + // Check the text elements' l10n IDs again. + for (let [id, { enabled, disabled }] of Object.entries(EXPECTED_L10N_IDS)) { + Assert.equal( + doc.getElementById(id).dataset.l10nId, + newExpectedVisibility ? enabled : disabled, + `New l10n ID for element with ID ${id}, newExpectedVisibility=${newExpectedVisibility}` + ); + } + + // Clean up. + gBrowser.removeCurrentTab(); + await QuickSuggestTestUtils.setScenario(null); +} + +// Verifies all 8 states of the 3 toggles and their related info box states. +add_task(async function togglesAndInfoBox() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-all"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored-sponsored"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored-data"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-sponsored-data"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox("addressbar-firefox-suggest-info-sponsored"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-data"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox(null); + await SpecialPowers.popPrefEnv(); + + gBrowser.removeCurrentTab(); +}); + +// Clicks each of the toggles and makes sure the prefs and info box are updated. +add_task(async function clickToggles() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let addressBarSection = doc.getElementById("locationBarGroup"); + addressBarSection.scrollIntoView(); + + async function clickToggle(id) { + let toggle = doc.getElementById(id); + let changed = BrowserTestUtils.waitForEvent(toggle, "toggle"); + let button = toggle.buttonEl; + await EventUtils.synthesizeMouseAtCenter( + button, + {}, + gBrowser.selectedBrowser.contentWindow + ); + await changed; + } + + // Set initial state. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-all"); + + // non-sponsored toggle + await clickToggle(NONSPONSORED_TOGGLE_ID); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ), + "suggest.quicksuggest.nonsponsored is false after clicking non-sponsored toggle" + ); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-sponsored-data"); + + // sponsored toggle + await clickToggle(SPONSORED_TOGGLE_ID); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ), + "suggest.quicksuggest.nonsponsored remains false after clicking sponsored toggle" + ); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.sponsored" + ), + "suggest.quicksuggest.sponsored is false after clicking sponsored toggle" + ); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-data"); + + // data collection toggle + await clickToggle(DATA_COLLECTION_TOGGLE_ID); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ), + "suggest.quicksuggest.nonsponsored remains false after clicking sponsored toggle" + ); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.sponsored" + ), + "suggest.quicksuggest.sponsored remains false after clicking data collection toggle" + ); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.quicksuggest.dataCollection.enabled" + ), + "quicksuggest.dataCollection.enabled is false after clicking data collection toggle" + ); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox(null); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Clicks the learn-more links and checks the help page is opened in a new tab. +add_task(async function clickLearnMore() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let addressBarSection = doc.getElementById("locationBarGroup"); + addressBarSection.scrollIntoView(); + + // Set initial state so that the info box and learn more link are shown. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + await assertInfoBox("addressbar-firefox-suggest-info-all"); + + let learnMoreLinks = doc.querySelectorAll("." + LEARN_MORE_CLASS); + Assert.equal( + learnMoreLinks.length, + 3, + "Expected number of learn-more links are present" + ); + for (let link of learnMoreLinks) { + Assert.ok( + BrowserTestUtils.is_visible(link), + "Learn-more link is visible: " + link.id + ); + } + + let prefsTab = gBrowser.selectedTab; + for (let link of learnMoreLinks) { + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + QuickSuggest.HELP_URL + ); + info("Clicking learn-more link: " + link.id); + Assert.ok(link.id, "Sanity check: Learn-more link has an ID"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + link.id, + {}, + gBrowser.selectedBrowser + ); + info("Waiting for help page to load in a new tab"); + await tabPromise; + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = prefsTab; + } + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Tests the visibility of the best match checkbox based on the values of +// `browser.urlbar.quicksuggest.enabled` and `browser.urlbar.bestMatch.enabled`. +add_task(async function bestMatchVisibility() { + for (let initialQuickSuggest of [false, true]) { + for (let initialBestMatch of [false, true]) { + for (let newQuickSuggest of [false, true]) { + for (let newBestMatch of [false, true]) { + await doBestMatchVisibilityTest({ + initialQuickSuggest, + initialBestMatch, + newQuickSuggest, + newBestMatch, + }); + } + } + } + } +}); + +// Tests the "Restore" button for dismissed suggestions. +add_task(async function restoreDismissedSuggestions() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let addressBarSection = doc.getElementById("locationBarGroup"); + addressBarSection.scrollIntoView(); + + let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID); + Assert.equal( + Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""), + "", + "Block list is empty initially" + ); + Assert.ok( + Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED), + "Weather suggestions are enabled initially" + ); + Assert.ok(button.disabled, "Restore button is disabled initially."); + + await QuickSuggest.blockedSuggestions.add("https://example.com/"); + Assert.notEqual( + Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""), + "", + "Block list is non-empty after adding URL" + ); + Assert.ok(!button.disabled, "Restore button is enabled after blocking URL."); + button.click(); + Assert.equal( + Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""), + "", + "Block list is empty clicking Restore button" + ); + Assert.ok(button.disabled, "Restore button is disabled after clicking it."); + + Services.prefs.setBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED, false); + Assert.ok( + !button.disabled, + "Restore button is enabled after disabling weather suggestions." + ); + button.click(); + Assert.ok( + Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED), + "Weather suggestions are enabled after clicking Restore button" + ); + Assert.ok( + button.disabled, + "Restore button is disabled after clicking it again." + ); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/** + * Runs a test that checks the visibility of the Firefox Suggest best match + * checkbox. It does the following: + * + * 1. Sets the quick suggest and best match feature prefs + * 2. Opens about:preferences and checks the visibility of the checkbox + * 3. Sets the quick suggest and best match feature prefs again + * 4. Checks the visibility of the checkbox again + * + * @param {boolean} initialQuickSuggest + * The value to set for `browser.urlbar.quicksuggest.enabled` before + * about:preferences is opened. + * @param {boolean} initialBestMatch + * The value to set for `browser.urlbar.bestMatch.enabled` before + * about:preferences is opened. + * @param {boolean} newQuickSuggest + * The value to set for `browser.urlbar.quicksuggest.enabled` while + * about:preferences is open. + * @param {boolean} newBestMatch + * The value to set for `browser.urlbar.bestMatch.enabled` while + * about:preferences is open. + */ +async function doBestMatchVisibilityTest({ + initialQuickSuggest, + initialBestMatch, + newQuickSuggest, + newBestMatch, +}) { + info( + "Running best match visibility test: " + + JSON.stringify( + { + initialQuickSuggest, + initialBestMatch, + newQuickSuggest, + newBestMatch, + }, + null, + 2 + ) + ); + + // Set the initial pref values. + Services.prefs.setBoolPref( + "browser.urlbar.quicksuggest.enabled", + initialQuickSuggest + ); + Services.prefs.setBoolPref( + "browser.urlbar.bestMatch.enabled", + initialBestMatch + ); + + // Open prefs and check the initial visibility. + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(BEST_MATCH_CONTAINER_ID); + Assert.equal( + BrowserTestUtils.is_visible(container), + initialBestMatch, + "The checkbox container has the expected initial visibility" + ); + + // Set the new pref values. + Services.prefs.setBoolPref( + "browser.urlbar.quicksuggest.enabled", + newQuickSuggest + ); + Services.prefs.setBoolPref("browser.urlbar.bestMatch.enabled", newBestMatch); + + // Check visibility again. + Assert.equal( + BrowserTestUtils.is_visible(container), + newBestMatch, + "The checkbox container has the expected visibility after setting prefs" + ); + + // Clean up. + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("browser.urlbar.quicksuggest.enabled"); + Services.prefs.clearUserPref("browser.urlbar.bestMatch.enabled"); +} + +// Tests the visibility of the best match checkbox when the best match feature +// is enabled via a Nimbus experiment before about:preferences is opened. +add_task(async function bestMatchVisibility_experiment_beforeOpen() { + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + bestMatchEnabled: true, + }, + callback: async () => { + await openPreferencesViaOpenPreferencesAPI("privacy", { + leaveOpen: true, + }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(BEST_MATCH_CONTAINER_ID); + Assert.ok( + BrowserTestUtils.is_visible(container), + "The checkbox container is visible" + ); + gBrowser.removeCurrentTab(); + }, + }); +}); + +// Tests the visibility of the best match checkbox when the best match feature +// is enabled via a Nimbus experiment after about:preferences is opened. +add_task(async function bestMatchVisibility_experiment_afterOpen() { + // Open prefs and check the initial visibility. + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(BEST_MATCH_CONTAINER_ID); + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The checkbox container is hidden initially" + ); + + // Install an experiment with best match enabled. + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + bestMatchEnabled: true, + }, + callback: () => { + Assert.ok( + BrowserTestUtils.is_visible(container), + "The checkbox container is visible after installing the experiment" + ); + }, + }); + + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The checkbox container is hidden again after the experiment is uninstalled" + ); + + gBrowser.removeCurrentTab(); +}); + +// Check the pref and the checkbox for best match. +add_task(async function bestMatchToggle() { + // Enable the feature so that the toggle appears. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + const doc = gBrowser.selectedBrowser.contentDocument; + const checkbox = doc.getElementById(BEST_MATCH_CHECKBOX_ID); + checkbox.scrollIntoView(); + + info("Check if the checkbox stauts reflects the pref value"); + for (const isEnabled of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.bestmatch", isEnabled]], + }); + assertPrefUIState({ [BEST_MATCH_CHECKBOX_ID]: isEnabled }, "checked"); + await SpecialPowers.popPrefEnv(); + } + + info("Check if the pref value reflects the checkbox status"); + for (let i = 0; i < 2; i++) { + const initialValue = checkbox.checked; + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + BEST_MATCH_CHECKBOX_ID, + {}, + gBrowser.selectedBrowser + ); + Assert.ok(initialValue !== checkbox.checked); + Assert.equal( + Services.prefs.getBoolPref("browser.urlbar.suggest.bestmatch"), + checkbox.checked + ); + } + + // Clean up. + Services.prefs.clearUserPref("browser.urlbar.suggest.bestmatch"); + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Clicks the learn-more link for best match and checks the help page is opened +// in a new tab. +add_task(async function clickBestMatchLearnMore() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const link = doc.getElementById("firefoxSuggestBestMatchLearnMore"); + Assert.ok(BrowserTestUtils.is_visible(link), "Learn-more link is visible"); + + const tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + QuickSuggest.HELP_URL + ); + + info("Clicking learn-more link"); + link.scrollIntoView(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#firefoxSuggestBestMatchLearnMore", + {}, + gBrowser.selectedBrowser + ); + + info("Waiting for help page to load in a new tab"); + const tab = await tabPromise; + gBrowser.removeTab(tab); + + // Clean up. + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/** + * Verifies the state of pref related toggles and checkboxes. + * + * @param {object} stateByElementID + * Maps toggle/checkbox element IDs to booleans. Each boolean + * is the expected state of the corresponding ID. + * @param {object} attr + * Attribute to check against the expected state. The "pressed" + * attribute is verified by default, since this is mostly used + * for toggle buttons. + */ +function assertPrefUIState(stateByElementID, attr = "pressed") { + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CONTAINER_ID); + Assert.ok(BrowserTestUtils.is_visible(container), "The container is visible"); + for (let [id, state] of Object.entries(stateByElementID)) { + let element = doc.getElementById(id); + Assert.equal(element[attr], state, "Expected state for ID: " + id); + } +} + +/** + * Verifies the state of the info box. + * + * @param {string} expectedL10nID + * The l10n ID of the string that should be visible in the info box, null if + * the info box should be hidden. + */ +async function assertInfoBox(expectedL10nID) { + info("Checking info box with expected l10n ID: " + expectedL10nID); + let doc = gBrowser.selectedBrowser.contentDocument; + let infoBox = doc.getElementById(INFO_BOX_ID); + await TestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(infoBox) == !!expectedL10nID, + "Waiting for expected info box visibility: " + !!expectedL10nID + ); + + let infoIcon = infoBox.querySelector(".info-icon"); + Assert.equal( + BrowserTestUtils.is_visible(infoIcon), + !!expectedL10nID, + "The info icon is visible iff a description should be shown" + ); + + let learnMore = infoBox.querySelector("." + LEARN_MORE_CLASS); + Assert.ok(learnMore, "Found the info box learn more link"); + Assert.equal( + BrowserTestUtils.is_visible(learnMore), + !!expectedL10nID, + "The info box learn more link is visible iff a description should be shown" + ); + + if (expectedL10nID) { + let infoText = doc.getElementById(INFO_TEXT_ID); + Assert.equal( + infoText.dataset.l10nId, + expectedL10nID, + "Info text has expected l10n ID" + ); + } +} diff --git a/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js new file mode 100644 index 0000000000..6a6e419229 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function initialState() { + // check pref permutations to verify the UI opens in the correct state + const prefTests = [ + { + initialPrefs: [ + ["signon.rememberSignons", true], + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ["signon.autofillForms", true], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.rememberSignons", true], + ["signon.generation.available", true], + ["signon.generation.enabled", false], + ["signon.autofillForms", false], + ], + expected: "unchecked", + }, + { + initialPrefs: [ + ["signon.rememberSignons", true], + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ], + expected: "hidden", + }, + { + initialPrefs: [ + ["signon.rememberSignons", false], + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ["signon.autofillForms", true], + ], + expected: "disabled", + }, + ]; + for (let test of prefTests) { + // set initial pref values + info("initialState, testing with: " + JSON.stringify(test)); + await SpecialPowers.pushPrefEnv({ set: test.initialPrefs }); + + // open about:privacy in a tab + // verify expected conditions + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + let doc = browser.contentDocument; + let generatePasswordsCheckbox = doc.getElementById("generatePasswords"); + let autofillFormsCheckbox = doc.getElementById( + "passwordAutofillCheckbox" + ); + doc.getElementById("passwordSettings").scrollIntoView(); + + info("initialState, assert on expected state:" + test.expected); + switch (test.expected) { + case "hidden": + is_element_hidden( + generatePasswordsCheckbox, + "#generatePasswords checkbox is hidden" + ); + break; + case "checked": + is_element_visible( + generatePasswordsCheckbox, + "#generatePasswords checkbox is visible" + ); + ok( + generatePasswordsCheckbox.checked, + "#generatePasswords checkbox is checked" + ); + ok( + autofillFormsCheckbox.checked, + "#passwordAutofillCheckbox is checked" + ); + break; + case "unchecked": + ok( + !generatePasswordsCheckbox.checked, + "#generatePasswords checkbox is un-checked" + ); + ok( + !autofillFormsCheckbox.checked, + "#passwordAutofillCheckbox is un-checked" + ); + break; + case "disabled": + ok( + generatePasswordsCheckbox.disabled, + "#generatePasswords checkbox is disabled" + ); + ok( + autofillFormsCheckbox.disabled, + "#passwordAutofillCheckbox is disabled" + ); + break; + default: + ok(false, "Unknown expected state: " + test.expected); + } + } + ); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function toggleGenerationEnabled() { + // clicking the checkbox should toggle the pref + SpecialPowers.pushPrefEnv({ + set: [ + ["signon.generation.available", true], + ["signon.generation.enabled", false], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + let doc = browser.contentDocument; + let checkbox = doc.getElementById("generatePasswords"); + + info("waiting for the browser to have focus"); + await SimpleTest.promiseFocus(browser); + let prefChanged = TestUtils.waitForPrefChange( + "signon.generation.enabled" + ); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + checkbox.focus(); + is(doc.activeElement, checkbox, "checkbox is focused"); + EventUtils.synthesizeKey(" "); + + info("waiting for pref to change"); + await prefChanged; + ok(checkbox.checked, "#generatePasswords checkbox is checked"); + ok( + Services.prefs.getBoolPref("signon.generation.enabled"), + "enabled pref is now true" + ); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function toggleRememberSignon() { + // toggling rememberSignons checkbox should make generation checkbox disabled + SpecialPowers.pushPrefEnv({ + set: [ + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + let doc = browser.contentDocument; + let checkbox = doc.getElementById("savePasswords"); + let generationCheckbox = doc.getElementById("generatePasswords"); + + ok( + !generationCheckbox.disabled, + "generation checkbox is not initially disabled" + ); + + info("waiting for the browser to have focus"); + await SimpleTest.promiseFocus(browser); + let prefChanged = TestUtils.waitForPrefChange("signon.rememberSignons"); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + checkbox.focus(); + is(doc.activeElement, checkbox, "checkbox is focused"); + EventUtils.synthesizeKey(" "); + + info("waiting for pref to change"); + await prefChanged; + ok(!checkbox.checked, "#savePasswords checkbox is un-checked"); + ok(generationCheckbox.disabled, "generation checkbox becomes disabled"); + } + ); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_privacy_quickactions.js b/browser/components/preferences/tests/browser_privacy_quickactions.js new file mode 100644 index 0000000000..14f907cc5c --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_quickactions.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This tests the Privacy pane's Firefox QuickActions UI. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.quickactions.enabled", true], + ], + }); + + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction"], + label: "quickactions-downloads2", + }); + + registerCleanupFunction(() => { + UrlbarProviderQuickActions.removeAction("testaction"); + }); +}); + +async function isGroupHidden(tab) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [], + async () => content.document.getElementById("quickActionsBox").hidden + ); +} + +add_task(async function test_show_prefs() { + Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", false); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#privacy" + ); + + Assert.ok( + await isGroupHidden(tab), + "The preferences are hidden when pref disabled" + ); + + Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", true); + + Assert.ok( + !(await isGroupHidden(tab)), + "The preferences are shown when pref enabled" + ); + + Services.prefs.clearUserPref("browser.urlbar.quickactions.showPrefs"); + await BrowserTestUtils.removeTab(tab); +}); + +async function testActionIsShown(window) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testact", + waitForFocus: SimpleTest.waitForFocus, + }); + try { + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + return result.providerName == "quickactions"; + } catch (e) { + return false; + } +} + +add_task(async function test_prefs() { + Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#privacy" + ); + + Assert.ok( + !(await testActionIsShown(window)), + "Actions are not shown while pref disabled" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + let checkbox = content.document.getElementById("enableQuickActions"); + is( + checkbox.checked, + false, + "Checkbox is not checked while feature is disabled" + ); + checkbox.click(); + }); + + Assert.ok( + await testActionIsShown(window), + "Actions are shown after user clicks checkbox" + ); + + Services.prefs.clearUserPref("browser.urlbar.quickactions.showPrefs"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_privacy_relayIntegration.js b/browser/components/preferences/tests/browser_privacy_relayIntegration.js new file mode 100644 index 0000000000..23d62f38bd --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_relayIntegration.js @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function initialState() { + // check pref permutations to verify the UI opens in the correct state + const prefTests = [ + { + initialPrefs: [ + ["signon.firefoxRelay.feature", undefined], + ["signon.rememberSignons", true], + ], + expected: "hidden", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "available"], + ["signon.rememberSignons", true], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "enabled"], + ["signon.rememberSignons", true], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "disabled"], + ["signon.rememberSignons", true], + ], + expected: "unchecked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", undefined], + ["signon.rememberSignons", false], + ], + expected: "hidden", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "available"], + ["signon.rememberSignons", false], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "enabled"], + ["signon.rememberSignons", false], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "disabled"], + ["signon.rememberSignons", false], + ], + expected: "unchecked", + }, + ]; + for (let test of prefTests) { + // set initial pref values + info("initialState, testing with: " + JSON.stringify(test)); + await SpecialPowers.pushPrefEnv({ set: test.initialPrefs }); + + // open about:privacy in a tab + // verify expected conditions + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + const doc = browser.contentDocument; + const relayGroup = doc.getElementById("relayIntegrationBox"); + const checkbox = doc.getElementById("relayIntegration"); + const savePasswords = doc.getElementById("savePasswords"); + doc.getElementById("passwordSettings").scrollIntoView(); + + Assert.equal( + checkbox.disabled, + !savePasswords.checked, + "#relayIntegration checkbox disabled when #passwordAutofillCheckbox is unchecked" + ); + + switch (test.expected) { + case "hidden": + is_element_hidden(relayGroup, "#relayIntegrationBox is hidden"); + break; + case "checked": + is_element_visible(relayGroup, "#relayIntegrationBox is visible"); + Assert.ok( + checkbox.checked, + "#relayIntegration checkbox is checked" + ); + break; + case "unchecked": + is_element_visible(relayGroup, "#relayIntegrationBox is visible"); + Assert.ok( + !checkbox.checked, + "#relayIntegration checkbox is un-checked" + ); + break; + default: + Assert.ok(false, "Unknown expected state: " + test.expected); + break; + } + } + ); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function toggleRelayIntegration() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.firefoxRelay.feature", "enabled"], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async browser => { + await SimpleTest.promiseFocus(browser); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + const doc = browser.contentDocument; + const relayCheckbox = doc.getElementById("relayIntegration"); + relayCheckbox.focus(); + Assert.equal(doc.activeElement, relayCheckbox, "checkbox is focused"); + Assert.equal( + relayCheckbox.checked, + true, + "#relayIntegration checkbox is not checked" + ); + + async function clickOnFeatureCheckbox( + expectedPrefValue, + expectedCheckValue, + message + ) { + const prefChanged = TestUtils.waitForPrefChange( + "signon.firefoxRelay.feature" + ); + EventUtils.synthesizeKey(" "); + await prefChanged; + Assert.equal( + Services.prefs.getStringPref("signon.firefoxRelay.feature"), + expectedPrefValue, + message + ); + Assert.equal( + relayCheckbox.checked, + expectedCheckValue, + `#relayIntegration checkbox is ${ + expectedCheckValue ? "checked" : "unchecked" + }` + ); + } + + await clickOnFeatureCheckbox( + "disabled", + false, + 'Turn integration off from "enabled" feature state' + ); + await clickOnFeatureCheckbox( + "available", + true, + 'Turn integration on from "enabled" feature state' + ); + await clickOnFeatureCheckbox( + "disabled", + false, + 'Turn integration off from "enabled" feature state' + ); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function toggleRememberSignon() { + // toggling rememberSignons checkbox should make generation checkbox disabled + SpecialPowers.pushPrefEnv({ + set: [ + ["signon.firefoxRelay.feature", "available"], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + const doc = browser.contentDocument; + const checkbox = doc.getElementById("savePasswords"); + const relayCheckbox = doc.getElementById("relayIntegration"); + + Assert.ok( + !relayCheckbox.disabled, + "generation checkbox is not initially disabled" + ); + + await SimpleTest.promiseFocus(browser); + const prefChanged = TestUtils.waitForPrefChange("signon.rememberSignons"); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + checkbox.focus(); + Assert.equal(doc.activeElement, checkbox, "checkbox is focused"); + EventUtils.synthesizeKey(" "); + + await prefChanged; + Assert.ok(!checkbox.checked, "#savePasswords checkbox is un-checked"); + Assert.ok( + relayCheckbox.disabled, + "Relay integration checkbox becomes disabled" + ); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testLockedRelayPreference() { + // Locking relay preference should disable checkbox + Services.prefs.lockPref("signon.firefoxRelay.feature"); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + const doc = browser.contentDocument; + const relayCheckbox = doc.getElementById("relayIntegration"); + + Assert.ok(relayCheckbox.disabled, "Relay checkbox should be disabled"); + } + ); + + Services.prefs.unlockPref("signon.firefoxRelay.feature"); +}); diff --git a/browser/components/preferences/tests/browser_privacy_segmentation_pref.js b/browser/components/preferences/tests/browser_privacy_segmentation_pref.js new file mode 100644 index 0000000000..9b71d91e11 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_segmentation_pref.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the privacy segmentation pref and preferences UI. + +"use strict"; + +const PREF = "browser.dataFeatureRecommendations.enabled"; +const PREF_VISIBILITY = "browser.privacySegmentation.preferences.show"; + +add_task(async function test_preferences_section() { + if (!AppConstants.MOZ_DATA_REPORTING) { + ok(true, "Skipping test because data reporting is disabled"); + return; + } + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let section = doc.getElementById("privacySegmentationSection"); + let sectionHeader = section.querySelector("h2"); + let sectionDescription = section.querySelector("label"); + let radioGroup = section.querySelector( + "#privacyDataFeatureRecommendationRadioGroup" + ); + let radioEnabled = radioGroup.querySelector( + "#privacyDataFeatureRecommendationEnabled" + ); + let radioDisabled = radioGroup.querySelector( + "#privacyDataFeatureRecommendationDisabled" + ); + + for (let show of [false, true]) { + Services.prefs.setBoolPref(PREF_VISIBILITY, show); + let showStr = show ? "visible" : "hidden"; + + is( + BrowserTestUtils.is_visible(section), + show, + `Privacy Segmentation section should be ${showStr}.` + ); + is( + BrowserTestUtils.is_visible(sectionHeader), + show, + `Privacy Segmentation section header should be ${showStr}.` + ); + is( + BrowserTestUtils.is_visible(sectionDescription), + show, + `Privacy Segmentation section description should be ${showStr}.` + ); + is( + BrowserTestUtils.is_visible(radioGroup), + show, + `Privacy Segmentation radio group should be ${showStr}.` + ); + + // The section is visible, test radio buttons. + if (show) { + Services.prefs.setBoolPref(PREF, false); + + is( + radioGroup.value, + "false", + "Radio group should reflect initial pref state of false." + ); + + info("Selecting radio on."); + radioEnabled.click(); + is( + Services.prefs.getBoolPref(PREF), + true, + "Privacy Segmentation should be enabled." + ); + + info("Selecting radio off."); + radioDisabled.click(); + is( + Services.prefs.getBoolPref(PREF), + false, + "Privacy Segmentation should be disabled." + ); + + info("Updating pref externally"); + is( + radioGroup.value, + "false", + "Radio group should reflect initial pref state of false." + ); + Services.prefs.setBoolPref(PREF, true); + await BrowserTestUtils.waitForMutationCondition( + radioGroup, + { attributeFilter: ["value"] }, + () => radioGroup.value == "true" + ); + is( + radioGroup.value, + "true", + "Updating Privacy Segmentation pref also updates radio group." + ); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Services.prefs.clearUserPref(PREF_VISIBILITY); + Services.prefs.clearUserPref(PREF); +}); + +add_task(async function test_preferences_section_data_reporting_disabled() { + if (AppConstants.MOZ_DATA_REPORTING) { + ok(true, "Skipping test because data reporting is enabled"); + return; + } + + for (let show of [false, true]) { + Services.prefs.setBoolPref(PREF_VISIBILITY, show); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let section = doc.getElementById("privacySegmentationSection"); + is( + !!section, + show, + "Section should only exist when privacy segmentation section is enabled." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + Services.prefs.clearUserPref(PREF_VISIBILITY); +}); diff --git a/browser/components/preferences/tests/browser_privacy_syncDataClearing.js b/browser/components/preferences/tests/browser_privacy_syncDataClearing.js new file mode 100644 index 0000000000..d5b6b904ab --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_syncDataClearing.js @@ -0,0 +1,287 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * With no custom cleaning categories set and sanitizeOnShutdown disabled, + * the checkboxes "alwaysClear" and "deleteOnClose" should share the same state. + * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box. + */ +add_task(async function test_syncWithoutCustomPrefs() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected"); + + deleteOnCloseBox.click(); + + ok(deleteOnCloseBox.checked, "DeleteOnClose is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "DeleteOnClose sets alwaysClear in the same state, selected" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is set" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"), + "Downloads cleaning pref is not set" + ); + + deleteOnCloseBox.click(); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "DeleteOnclose sets alwaysClear in the same state, deselected" + ); + + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"), + "Downloads cleaning pref is not set" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Services.prefs.clearUserPref("privacy.clearOnShutdown.downloads"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.offlineApps"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cache"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cookies"); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); +}); + +/* + * With custom cleaning category already set and SanitizeOnShutdown enabled, + * deselecting "deleteOnClose" should not change the state of "alwaysClear". + * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box. + */ +add_task(async function test_syncWithCustomPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.history", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(alwaysClearBox.checked, "AlwaysClear initial state is selected"); + + deleteOnCloseBox.click(); + + ok(deleteOnCloseBox.checked, "DeleteOnClose is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, selected" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.history"), + "History cleaning pref is still set" + ); + + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is set" + ); + + deleteOnCloseBox.click(); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected"); + is( + !deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear is not synced with deleteOnClose, only deleteOnClose is deselected" + ); + + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is reset" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.history"), + "History cleaning pref is still set" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +/* + * Setting/resetting cleaning prefs for cookies, cache, offline apps + * and selecting/deselecting the "alwaysClear" Box, also selects/deselects + * the "deleteOnClose" box. + */ + +add_task(async function test_syncWithCustomPrefs() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", true], + ["privacy.clearOnShutdown.cache", true], + ["privacy.clearOnShutdown.offlineApps", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + ok(alwaysClearBox.checked, "AlwaysClear is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, selected" + ); + + alwaysClearBox.click(); + + ok(!alwaysClearBox.checked, "AlwaysClear is deselected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, deselected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +/* + * On loading the page, the ClearOnClose box should be set according to the pref selection + */ +add_task(async function test_initialState() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", true], + ["privacy.clearOnShutdown.cache", true], + ["privacy.clearOnShutdown.offlineApps", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the prefs, selected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", false], + ["privacy.clearOnShutdown.cache", false], + ["privacy.clearOnShutdown.offlineApps", false], + ["privacy.sanitize.sanitizeOnShutdown", true], + ["privacy.clearOnShutdown.history", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + document = gBrowser.contentDocument; + deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + !deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the prefs, deselected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // When private browsing mode autostart is selected, the deleteOnClose Box is selected always + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", false], + ["privacy.clearOnShutdown.cache", false], + ["privacy.clearOnShutdown.offlineApps", false], + ["privacy.sanitize.sanitizeOnShutdown", false], + ["browser.privatebrowsing.autostart", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + document = gBrowser.contentDocument; + deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the private Browsing autostart pref, selected" + ); + + // Reset history mode + let historyMode = document.getElementById("historyMode"); + historyMode.value = "remember"; + historyMode.doCommand(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_privacypane_2.js b/browser/components/preferences/tests/browser_privacypane_2.js new file mode 100644 index 0000000000..46fb3347c8 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacypane_2.js @@ -0,0 +1,19 @@ +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from privacypane_tests_perwindow.js */ +Services.scriptloader.loadSubScript( + rootDir + "privacypane_tests_perwindow.js", + this +); + +run_test_subset([ + test_pane_visibility, + test_dependent_elements, + test_dependent_cookie_elements, + test_dependent_clearonclose_elements, + test_dependent_prefs, +]); diff --git a/browser/components/preferences/tests/browser_privacypane_3.js b/browser/components/preferences/tests/browser_privacypane_3.js new file mode 100644 index 0000000000..e88a6059eb --- /dev/null +++ b/browser/components/preferences/tests/browser_privacypane_3.js @@ -0,0 +1,21 @@ +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from privacypane_tests_perwindow.js */ +Services.scriptloader.loadSubScript( + rootDir + "privacypane_tests_perwindow.js", + this +); + +run_test_subset([ + test_custom_retention("rememberHistory", "remember"), + test_custom_retention("rememberHistory", "custom"), + test_custom_retention("rememberForms", "custom"), + test_custom_retention("rememberForms", "custom"), + test_historymode_retention("remember", "custom"), + test_custom_retention("alwaysClear", "remember"), + test_custom_retention("alwaysClear", "custom"), +]); diff --git a/browser/components/preferences/tests/browser_proxy_backup.js b/browser/components/preferences/tests/browser_proxy_backup.js new file mode 100644 index 0000000000..fc05e19ada --- /dev/null +++ b/browser/components/preferences/tests/browser_proxy_backup.js @@ -0,0 +1,84 @@ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + // network.proxy.type needs to be backed up and restored because mochitest + // changes this setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function () { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + for (let proxyType of ["http", "ssl", "socks"]) { + Services.prefs.clearUserPref("network.proxy." + proxyType); + Services.prefs.clearUserPref("network.proxy." + proxyType + "_port"); + if (proxyType == "http") { + continue; + } + Services.prefs.clearUserPref("network.proxy.backup." + proxyType); + Services.prefs.clearUserPref( + "network.proxy.backup." + proxyType + "_port" + ); + } + // On accepting the dialog, we also write TRR values, so we need to clear + // them. They are tested separately in browser_privacy_dnsoverhttps.js. + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + }); + + let connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + + // Set a shared proxy and an SSL backup + Services.prefs.setIntPref("network.proxy.type", 1); + Services.prefs.setBoolPref("network.proxy.share_proxy_settings", true); + Services.prefs.setCharPref("network.proxy.http", "example.com"); + Services.prefs.setIntPref("network.proxy.http_port", 1200); + Services.prefs.setCharPref("network.proxy.ssl", "example.com"); + Services.prefs.setIntPref("network.proxy.ssl_port", 1200); + Services.prefs.setCharPref("network.proxy.backup.ssl", "127.0.0.1"); + Services.prefs.setIntPref("network.proxy.backup.ssl_port", 9050); + + /* + The connection dialog alone won't save onaccept since it uses type="child", + so it has to be opened as a sub dialog of the main pref tab. + Open the main tab here. + */ + open_preferences(async function tabOpened(aContentWindow) { + is( + gBrowser.currentURI.spec, + "about:preferences", + "about:preferences loaded" + ); + let dialog = await openAndLoadSubDialog(connectionURL); + let dialogElement = dialog.document.getElementById("ConnectionsDialog"); + let dialogClosingPromise = BrowserTestUtils.waitForEvent( + dialogElement, + "dialogclosing" + ); + + ok(dialog, "connection window opened"); + dialogElement.acceptDialog(); + + let dialogClosingEvent = await dialogClosingPromise; + ok(dialogClosingEvent, "connection window closed"); + + // The SSL backup should not be replaced by the shared value + is( + Services.prefs.getCharPref("network.proxy.backup.ssl"), + "127.0.0.1", + "Shared proxy backup shouldn't be replaced" + ); + is( + Services.prefs.getIntPref("network.proxy.backup.ssl_port"), + 9050, + "Shared proxy port backup shouldn't be replaced" + ); + + gBrowser.removeCurrentTab(); + finish(); + }); +} diff --git a/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js b/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js new file mode 100644 index 0000000000..2a7cb2d10e --- /dev/null +++ b/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js @@ -0,0 +1,47 @@ +"use strict"; + +function switchToCustomHistoryMode(doc) { + // Select the last item in the menulist. + let menulist = doc.getElementById("historyMode"); + menulist.focus(); + EventUtils.sendKey("UP"); +} + +function testPrefStateMatchesLockedState() { + let win = gBrowser.contentWindow; + let doc = win.document; + switchToCustomHistoryMode(doc); + + let checkbox = doc.getElementById("alwaysClear"); + let preference = win.Preferences.get("privacy.sanitize.sanitizeOnShutdown"); + is( + checkbox.disabled, + preference.locked, + "Always Clear checkbox should be enabled when preference is not locked." + ); + + Services.prefs.clearUserPref("privacy.history.custom"); + gBrowser.removeCurrentTab(); +} + +add_setup(function () { + registerCleanupFunction(function resetPreferences() { + Services.prefs.unlockPref("privacy.sanitize.sanitizeOnShutdown"); + Services.prefs.clearUserPref("privacy.history.custom"); + }); +}); + +add_task(async function test_preference_enabled_when_unlocked() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + testPrefStateMatchesLockedState(); +}); + +add_task(async function test_preference_disabled_when_locked() { + Services.prefs.lockPref("privacy.sanitize.sanitizeOnShutdown"); + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + testPrefStateMatchesLockedState(); +}); diff --git a/browser/components/preferences/tests/browser_searchChangedEngine.js b/browser/components/preferences/tests/browser_searchChangedEngine.js new file mode 100644 index 0000000000..0882c9775e --- /dev/null +++ b/browser/components/preferences/tests/browser_searchChangedEngine.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); +const { SearchUtils } = ChromeUtils.importESModule( + "resource://gre/modules/SearchUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); +SearchTestUtils.init(this); + +function findRow(tree, expectedName) { + for (let i = 0; i < tree.view.rowCount; i++) { + let name = tree.view.getCellText( + i, + tree.columns.getNamedColumn("engineName") + ); + + if (name == expectedName) { + return i; + } + } + return -1; +} + +add_task(async function test_change_engine() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + + await SearchTestUtils.installSearchExtension({ + id: "example@tests.mozilla.org", + name: "Example", + version: "1.0", + keyword: "foo", + favicon_url: "img123.png", + }); + + let tree = doc.querySelector("#engineList"); + + let row = findRow(tree, "Example"); + + Assert.notEqual(row, -1, "Should have found the entry"); + Assert.ok( + tree.view + .getImageSrc(row, tree.columns.getNamedColumn("engineName")) + .includes("img123.png"), + "Should have the correct image URL" + ); + Assert.equal( + tree.view.getCellText(row, tree.columns.getNamedColumn("engineKeyword")), + "foo", + "Should show the correct keyword" + ); + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + await SearchTestUtils.installSearchExtension({ + id: "example@tests.mozilla.org", + name: "Example 2", + version: "2.0", + keyword: "bar", + favicon_url: "img456.png", + }); + await updatedPromise; + + row = findRow(tree, "Example 2"); + + Assert.notEqual(row, -1, "Should have found the updated entry"); + Assert.ok( + tree.view + .getImageSrc(row, tree.columns.getNamedColumn("engineName")) + .includes("img456.png"), + "Should have the correct image URL" + ); + Assert.equal( + tree.view.getCellText(row, tree.columns.getNamedColumn("engineKeyword")), + "bar", + "Should show the correct keyword" + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchDefaultEngine.js b/browser/components/preferences/tests/browser_searchDefaultEngine.js new file mode 100644 index 0000000000..e64f88fab3 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchDefaultEngine.js @@ -0,0 +1,372 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); + +SearchTestUtils.init(this); + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + name: "engine1", + search_url: "https://example.com/engine1", + search_url_get_params: "search={searchTerms}", + }); + await SearchTestUtils.installSearchExtension({ + name: "engine2", + search_url: "https://example.com/engine2", + search_url_get_params: "search={searchTerms}", + }); + + const defaultEngine = await Services.search.getDefault(); + const defaultPrivateEngine = await Services.search.getDefaultPrivate(); + + registerCleanupFunction(async () => { + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + defaultPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); +}); + +add_task(async function test_openWithPrivateDefaultNotEnabledFirst() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.search.separatePrivateDefault", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const separateEngineCheckbox = doc.getElementById( + "browserSeparateDefaultEngine" + ); + const privateDefaultVbox = doc.getElementById( + "browserPrivateEngineSelection" + ); + + Assert.ok( + separateEngineCheckbox.hidden, + "Should have hidden the separate search engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should have hidden the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", true]], + }); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should have displayed the separate search engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should not have displayed the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", true]], + }); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should still be displaying the separate search engine checkbox" + ); + Assert.ok( + !privateDefaultVbox.hidden, + "Should have displayed the private engine selection box" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_openWithPrivateDefaultEnabledFirst() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const separateEngineCheckbox = doc.getElementById( + "browserSeparateDefaultEngine" + ); + const privateDefaultVbox = doc.getElementById( + "browserPrivateEngineSelection" + ); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should not have hidden the separate search engine checkbox" + ); + Assert.ok( + !privateDefaultVbox.hidden, + "Should not have hidden the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", false]], + }); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should not have hidden the separate search engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should have hidden the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", false]], + }); + + Assert.ok( + separateEngineCheckbox.hidden, + "Should have hidden the separate private engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should still be hiding the private engine selection box" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_separatePrivateDefault() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const separateEngineCheckbox = doc.getElementById( + "browserSeparateDefaultEngine" + ); + const privateDefaultVbox = doc.getElementById( + "browserPrivateEngineSelection" + ); + + Assert.ok( + privateDefaultVbox.hidden, + "Should not be displaying the private engine selection box" + ); + + separateEngineCheckbox.checked = false; + separateEngineCheckbox.doCommand(); + + Assert.ok( + Services.prefs.getBoolPref("browser.search.separatePrivateDefault"), + "Should have correctly set the pref" + ); + + Assert.ok( + !privateDefaultVbox.hidden, + "Should be displaying the private engine selection box" + ); + + separateEngineCheckbox.checked = true; + separateEngineCheckbox.doCommand(); + + Assert.ok( + !Services.prefs.getBoolPref("browser.search.separatePrivateDefault"), + "Should have correctly turned the pref off" + ); + + Assert.ok( + privateDefaultVbox.hidden, + "Should have hidden the private engine selection box" + ); + + gBrowser.removeCurrentTab(); +}); + +async function setDefaultEngine( + testPrivate, + currentEngineName, + expectedEngineName +) { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const defaultEngineSelector = doc.getElementById( + testPrivate ? "defaultPrivateEngine" : "defaultEngine" + ); + + Assert.equal( + defaultEngineSelector.selectedItem.engine.name, + currentEngineName, + "Should have the correct engine as default on first open" + ); + + const popup = defaultEngineSelector.menupopup; + const popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + defaultEngineSelector, + {}, + defaultEngineSelector.ownerGlobal + ); + await popupShown; + + const items = Array.from(popup.children); + const engine2Item = items.find( + item => item.engine.name == expectedEngineName + ); + + const defaultChanged = SearchTestUtils.promiseSearchNotification( + testPrivate ? "engine-default-private" : "engine-default", + "browser-search-engine-modified" + ); + // Waiting for popupHiding here seemed to cause a race condition, however + // as we're really just interested in the notification, we'll just use + // that here. + EventUtils.synthesizeMouseAtCenter(engine2Item, {}, engine2Item.ownerGlobal); + await defaultChanged; + + const newDefault = testPrivate + ? await Services.search.getDefaultPrivate() + : await Services.search.getDefault(); + Assert.equal( + newDefault.name, + expectedEngineName, + "Should have changed the default engine to engine2" + ); +} + +add_task(async function test_setDefaultEngine() { + const engine1 = Services.search.getEngineByName("engine1"); + + // Set an initial default so we have a known engine. + await Services.search.setDefault( + engine1, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); + + await setDefaultEngine(false, "engine1", "engine2"); + + TelemetryTestUtils.assertEvents( + [ + { + object: "change_default", + value: "user", + extra: { + prev_id: engine1.telemetryId, + new_id: "other-engine2", + new_name: "engine2", + new_load_path: "[addon]engine2@tests.mozilla.org", + new_sub_url: "", + }, + }, + ], + { category: "search", method: "engine" } + ); + + let snapshot = await Glean.searchEngineDefault.changed.testGetValue(); + delete snapshot[0].timestamp; + Assert.deepEqual( + snapshot[0], + { + category: "search.engine.default", + name: "changed", + extra: { + change_source: "user", + previous_engine_id: engine1.telemetryId, + new_engine_id: "other-engine2", + new_display_name: "engine2", + new_load_path: "[addon]engine2@tests.mozilla.org", + new_submission_url: "", + }, + }, + "Should have received the correct event details" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_setPrivateDefaultEngine() { + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", true], + ], + }); + + const engine2 = Services.search.getEngineByName("engine2"); + + // Set an initial default so we have a known engine. + await Services.search.setDefaultPrivate( + engine2, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); + + await setDefaultEngine(true, "engine2", "engine1"); + + TelemetryTestUtils.assertEvents( + [ + { + object: "change_private", + value: "user", + extra: { + prev_id: engine2.telemetryId, + new_id: "other-engine1", + new_name: "engine1", + new_load_path: "[addon]engine1@tests.mozilla.org", + new_sub_url: "", + }, + }, + ], + { category: "search", method: "engine" } + ); + + let snapshot = await Glean.searchEnginePrivate.changed.testGetValue(); + delete snapshot[0].timestamp; + console.log(snapshot); + Assert.deepEqual( + snapshot[0], + { + category: "search.engine.private", + name: "changed", + extra: { + change_source: "user", + previous_engine_id: engine2.telemetryId, + new_engine_id: "other-engine1", + new_display_name: "engine1", + new_load_path: "[addon]engine1@tests.mozilla.org", + new_submission_url: "", + }, + }, + "Should have received the correct event details" + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchFindMoreLink.js b/browser/components/preferences/tests/browser_searchFindMoreLink.js new file mode 100644 index 0000000000..92864c9f54 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchFindMoreLink.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = "https://example.org/"; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.searchEnginesURL", TEST_URL]], + }); +}); + +add_task(async function test_click_find_more_link() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser); + + gBrowser.selectedBrowser.contentDocument + .getElementById("addEngines") + .scrollIntoView(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#addEngines", + {}, + gBrowser.selectedBrowser.browsingContext + ); + + let tab = await promiseNewTab; + Assert.equal( + tab.linkedBrowser.documentURI.spec, + TEST_URL, + "Should have loaded the expected page" + ); + + // Close both tabs. + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchRestoreDefaults.js b/browser/components/preferences/tests/browser_searchRestoreDefaults.js new file mode 100644 index 0000000000..80588d0cce --- /dev/null +++ b/browser/components/preferences/tests/browser_searchRestoreDefaults.js @@ -0,0 +1,259 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); +const { SearchUtils } = ChromeUtils.importESModule( + "resource://gre/modules/SearchUtils.sys.mjs" +); +add_task(async function test_restore_functionality() { + // Ensure no engines are hidden to begin with. + for (let engine of await Services.search.getAppProvidedEngines()) { + if (engine.hidden) { + engine.hidden = false; + } + } + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + + Assert.ok( + restoreDefaultsButton.disabled, + "Should have disabled the restore default search engines button on open" + ); + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + let tree = doc.querySelector("#engineList"); + // Check for default search engines to be displayed in the engineList + let defaultEngines = await Services.search.getAppProvidedEngines(); + for (let i = 0; i < defaultEngines.length; i++) { + let cellName = tree.view.getCellText( + i, + tree.columns.getNamedColumn("engineName") + ); + if (cellName == "DuckDuckGo") { + tree.view.selection.select(i); + break; + } + } + doc.getElementById("removeEngineButton").click(); + await updatedPromise; + + let engine = await Services.search.getEngineByName("DuckDuckGo"); + + Assert.ok(engine.hidden, "Should have hidden the engine"); + Assert.ok( + !restoreDefaultsButton.disabled, + "Should have enabled the restore default search engines button" + ); + + updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + // Let the stack unwind so that the restore defaults button can update its + // state. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + Assert.ok(!engine.hidden, "Should have re-enabled the disabled engine"); + Assert.ok( + restoreDefaultsButton.disabled, + "Should have disabled the restore default search engines button after use" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_restoreEnabledOnOpenWithEngineHidden() { + let engine = await Services.search.getEngineByName("DuckDuckGo"); + engine.hidden = true; + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + + Assert.ok( + !restoreDefaultsButton.disabled, + "Should have enabled the restore default search engines button on open" + ); + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + // Let the stack unwind so that the restore defaults button can update its + // state. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + Assert.ok(!engine.hidden, "Should have re-enabled the disabled engine"); + Assert.ok( + restoreDefaultsButton.disabled, + "Should have disabled the restore default search engines button after use" + ); + + gBrowser.removeCurrentTab(); +}); + +// This removes the last two engines and then the remaining engines from top to +// bottom, and then it restores the default engines. See bug 1681818. +add_task(async function test_removeOutOfOrder() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + Assert.ok( + restoreDefaultsButton.disabled, + "The restore-defaults button is disabled initially" + ); + + let tree = doc.querySelector("#engineList"); + let removeEngineButton = doc.getElementById("removeEngineButton"); + removeEngineButton.scrollIntoView(); + + let defaultEngines = await Services.search.getAppProvidedEngines(); + + // Remove the last two engines. After each removal, the selection should move + // to the first local shortcut. + for (let i = 0; i < 2; i++) { + tree.view.selection.select(defaultEngines.length - i - 1); + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + Assert.ok( + removeEngineButton.disabled, + "The remove-engine button is disabled because a local shortcut is selected" + ); + Assert.ok( + !restoreDefaultsButton.disabled, + "The restore-defaults button is enabled after removing an engine" + ); + } + + // Remove the remaining engines from top to bottom except for the default engine + // which can't be removed. + for (let i = 0; i < defaultEngines.length - 3; i++) { + tree.view.selection.select(0); + + if (defaultEngines[0].name == Services.search.defaultEngine.name) { + tree.view.selection.select(1); + } + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + Assert.ok( + !restoreDefaultsButton.disabled, + "The restore-defaults button is enabled after removing an engine" + ); + } + Assert.ok( + removeEngineButton.disabled, + "The remove-engine button is disabled because only one engine remains" + ); + + // Click the restore-defaults button. + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + + // Wait for the restore-defaults button to update its state. + await TestUtils.waitForCondition( + () => restoreDefaultsButton.disabled, + "Waiting for the restore-defaults button to become disabled" + ); + + Assert.ok( + restoreDefaultsButton.disabled, + "The restore-defaults button is disabled after restoring defaults" + ); + Assert.equal( + tree.view.rowCount, + defaultEngines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "All engines are restored" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_removeAndRestoreMultiple() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + let tree = doc.querySelector("#engineList"); + let removeEngineButton = doc.getElementById("removeEngineButton"); + removeEngineButton.scrollIntoView(); + + let defaultEngines = await Services.search.getAppProvidedEngines(); + + // Remove the second and fourth engines. + for (let i = 0; i < 2; i++) { + tree.view.selection.select(i * 2 + 1); + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + } + + // Click the restore-defaults button. + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + + // Remove the third engine. + tree.view.selection.select(3); + updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + + // Now restore again. + updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + + // Wait for the restore-defaults button to update its state. + await TestUtils.waitForCondition( + () => restoreDefaultsButton.disabled, + "Waiting for the restore-defaults button to become disabled" + ); + + Assert.equal( + tree.view.rowCount, + defaultEngines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "Should have the correct amount of engines" + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchScroll.js b/browser/components/preferences/tests/browser_searchScroll.js new file mode 100644 index 0000000000..ef3af646e9 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchScroll.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); +const { SearchUtils } = ChromeUtils.importESModule( + "resource://gre/modules/SearchUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); +SearchTestUtils.init(this); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.w3c_touch_events.enabled", 0]], + }); +}); + +add_task(async function test_scroll() { + info("Open preferences page for search"); + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const tree = doc.querySelector("#engineList"); + + info("Add engines to make the tree scrollable"); + for (let i = 0, n = parseInt(tree.getAttribute("rows")); i < n; i++) { + let extension = await SearchTestUtils.installSearchExtension({ + id: `${i}@tests.mozilla.org`, + name: `example${i}`, + version: "1.0", + keyword: `example${i}`, + }); + await AddonTestUtils.waitForSearchProviderStartup(extension); + } + + info("Make tree element move into viewport"); + const mainContent = doc.querySelector(".main-content"); + const readyForScrollIntoView = new Promise(r => { + mainContent.addEventListener("scroll", r, { once: true }); + }); + tree.scrollIntoView(); + await readyForScrollIntoView; + + const previousScroll = mainContent.scrollTop; + + await promiseMoveMouseAndScrollWheelOver(tree, 1, 1, false); + + Assert.equal( + previousScroll, + mainContent.scrollTop, + "Container element does not scroll" + ); + + info("Clean up"); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js new file mode 100644 index 0000000000..33ab63e8bf --- /dev/null +++ b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js @@ -0,0 +1,240 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const MAIN_PREF = "browser.search.suggest.enabled"; +const URLBAR_PREF = "browser.urlbar.suggest.searches"; +const FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst"; +const FIRST_CHECKBOX_ID = "showSearchSuggestionsFirstCheckbox"; + +add_setup(async function () { + // Make sure the main and urlbar suggestion prefs are enabled. + await SpecialPowers.pushPrefEnv({ + set: [ + [MAIN_PREF, true], + [URLBAR_PREF, true], + ], + }); +}); + +// Open preferences with search suggestions shown first (the default). +add_task(async function openWithSearchSuggestionsShownFirst() { + // Initially the pref should be true so search suggestions are shown first. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should be true initially" + ); + + // Open preferences. The checkbox should be checked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(FIRST_CHECKBOX_ID); + Assert.ok(checkbox.checked, "Checkbox should be checked"); + Assert.ok(!checkbox.disabled, "Checkbox should be enabled"); + + // Uncheck the checkbox. + checkbox.checked = false; + checkbox.doCommand(); + + // The pref should now be false so that history is shown first. + Assert.ok( + !Services.prefs.getBoolPref(FIRST_PREF), + "Pref should now be false to show history first" + ); + + // Make sure the checkbox state didn't change. + Assert.ok(!checkbox.checked, "Checkbox should remain unchecked"); + Assert.ok(!checkbox.disabled, "Checkbox should remain enabled"); + + // Clear the pref. + Services.prefs.clearUserPref(FIRST_PREF); + + // The checkbox should have become checked again. + Assert.ok( + checkbox.checked, + "Checkbox should become checked after clearing pref" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after clearing pref" + ); + + // Clean up. + gBrowser.removeCurrentTab(); +}); + +// Open preferences with history shown first. +add_task(async function openWithHistoryShownFirst() { + // Set the pref to show history first. + Services.prefs.setBoolPref(FIRST_PREF, false); + + // Open preferences. The checkbox should be unchecked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(FIRST_CHECKBOX_ID); + Assert.ok(!checkbox.checked, "Checkbox should be unchecked"); + Assert.ok(!checkbox.disabled, "Checkbox should be enabled"); + + // Check the checkbox. + checkbox.checked = true; + checkbox.doCommand(); + + // Make sure the checkbox state didn't change. + Assert.ok(checkbox.checked, "Checkbox should remain checked"); + Assert.ok(!checkbox.disabled, "Checkbox should remain enabled"); + + // The pref should now be true so that search suggestions are shown first. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should now be true to show search suggestions first" + ); + + // Set the pref to false again. + Services.prefs.setBoolPref(FIRST_PREF, false); + + // The checkbox should have become unchecked again. + Assert.ok( + !checkbox.checked, + "Checkbox should become unchecked after setting pref to false" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after setting pref to false" + ); + + // Clean up. + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref(FIRST_PREF); +}); + +// Checks how the show-suggestions-first pref and checkbox reacts to updates to +// URLBAR_PREF and MAIN_PREF. +add_task(async function superprefInteraction() { + // Initially the pref should be true so search suggestions are shown first. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should be true initially" + ); + + // Open preferences. The checkbox should be checked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(FIRST_CHECKBOX_ID); + Assert.ok(checkbox.checked, "Checkbox should be checked"); + Assert.ok(!checkbox.disabled, "Checkbox should be enabled"); + + // Two superior prefs control the show-suggestion-first pref: URLBAR_PREF and + // MAIN_PREF. Toggle each and make sure the show-suggestion-first checkbox + // reacts appropriately. + for (let superiorPref of [URLBAR_PREF, MAIN_PREF]) { + info(`Testing superior pref ${superiorPref}`); + + // Set the superior pref to false. + Services.prefs.setBoolPref(superiorPref, false); + + // The pref should remain true. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain true" + ); + + // The checkbox should have become unchecked and disabled. + Assert.ok( + !checkbox.checked, + "Checkbox should become unchecked after disabling urlbar suggestions" + ); + Assert.ok( + checkbox.disabled, + "Checkbox should become disabled after disabling urlbar suggestions" + ); + + // Set the superior pref to true. + Services.prefs.setBoolPref(superiorPref, true); + + // The pref should remain true. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain true" + ); + + // The checkbox should have become checked and enabled again. + Assert.ok( + checkbox.checked, + "Checkbox should become checked after re-enabling urlbar suggestions" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should become enabled after re-enabling urlbar suggestions" + ); + + // Set the pref to false. + Services.prefs.setBoolPref(FIRST_PREF, false); + + // The checkbox should have become unchecked. + Assert.ok( + !checkbox.checked, + "Checkbox should become unchecked after setting pref to false" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after setting pref to false" + ); + + // Set the superior pref to false again. + Services.prefs.setBoolPref(superiorPref, false); + + // The pref should remain false. + Assert.ok( + !Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain false" + ); + + // The checkbox should remain unchecked and become disabled. + Assert.ok( + !checkbox.checked, + "Checkbox should remain unchecked after disabling urlbar suggestions" + ); + Assert.ok( + checkbox.disabled, + "Checkbox should become disabled after disabling urlbar suggestions" + ); + + // Set the superior pref to true. + Services.prefs.setBoolPref(superiorPref, true); + + // The pref should remain false. + Assert.ok( + !Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain false" + ); + + // The checkbox should remain unchecked and become enabled. + Assert.ok( + !checkbox.checked, + "Checkbox should remain unchecked after re-enabling urlbar suggestions" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should become enabled after re-enabling urlbar suggestions" + ); + + // Finally, set the pref back to true. + Services.prefs.setBoolPref(FIRST_PREF, true); + + // The checkbox should have become checked. + Assert.ok( + checkbox.checked, + "Checkbox should become checked after setting pref back to true" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after setting pref back to true" + ); + } + + // Clean up. + gBrowser.removeCurrentTab(); + + Services.prefs.clearUserPref(FIRST_PREF); + Services.prefs.clearUserPref(URLBAR_PREF); + Services.prefs.clearUserPref(MAIN_PREF); +}); diff --git a/browser/components/preferences/tests/browser_search_no_results_change_category.js b/browser/components/preferences/tests/browser_search_no_results_change_category.js new file mode 100644 index 0000000000..131492632e --- /dev/null +++ b/browser/components/preferences/tests/browser_search_no_results_change_category.js @@ -0,0 +1,44 @@ +"use strict"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + let query = "ffff____noresults____ffff"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let noResultsEl = gBrowser.contentDocument.querySelector( + "#no-results-message" + ); + is_element_visible( + noResultsEl, + "Should be reporting no results for this query" + ); + + await gBrowser.contentWindow.gotoPref("panePrivacy"); + is_element_hidden( + noResultsEl, + "Should not be showing the 'no results' message after selecting a category" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_searchTerms.js b/browser/components/preferences/tests/browser_search_searchTerms.js new file mode 100644 index 0000000000..0af0591355 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_searchTerms.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + Tests the showSearchTerms option on the about:preferences#search page. +*/ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +const GROUP_ID = "searchbarGroup"; +const CHECKBOX_ID = "searchShowSearchTermCheckbox"; +const PREF_SEARCHTERMS = "browser.urlbar.showSearchTerms.enabled"; +const PREF_FEATUREGATE = "browser.urlbar.showSearchTerms.featureGate"; + +/* + If Nimbus experiment is enabled, check option visibility. +*/ +add_task(async function showSearchTermsVisibility_experiment_beforeOpen() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, false]], + }); + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + showSearchTermsFeatureGate: true, + }, + callback: async () => { + await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CHECKBOX_ID); + Assert.ok( + BrowserTestUtils.is_visible(container), + "The option box is visible" + ); + gBrowser.removeCurrentTab(); + }, + }); + await SpecialPowers.popPrefEnv(); +}); + +/* + If Nimbus experiment is not enabled initially but eventually enabled, + check option visibility on Preferences page. +*/ +add_task(async function showSearchTermsVisibility_experiment_afterOpen() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, false]], + }); + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CHECKBOX_ID); + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The option box is initially hidden." + ); + + // Install experiment. + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + showSearchTermsFeatureGate: true, + }, + callback: async () => { + Assert.ok( + BrowserTestUtils.is_visible(container), + "The option box is visible" + ); + }, + }); + + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The option box is hidden again after the experiment is uninstalled." + ); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/* + Check using the checkbox modifies the preference. +*/ +add_task(async function showSearchTerms_checkbox() { + // Enable the feature. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, true]], + }); + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + doc.getElementById(GROUP_ID).scrollIntoView(); + + let option = doc.getElementById(CHECKBOX_ID); + + // Evaluate checkbox pref is true. + Assert.ok(option.checked, "Option box should be checked."); + + // Evaluate checkbox when pref is false. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_SEARCHTERMS, false]], + }); + Assert.ok(!option.checked, "Option box should not be checked."); + await SpecialPowers.popPrefEnv(); + + // Evaluate pref when checkbox is un-checked. + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + CHECKBOX_ID, + {}, + gBrowser.selectedBrowser + ); + Assert.equal( + Services.prefs.getBoolPref(PREF_SEARCHTERMS), + false, + "Preference should be false if un-checked." + ); + + // Evaluate pref when checkbox is checked. + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + CHECKBOX_ID, + {}, + gBrowser.selectedBrowser + ); + Assert.equal( + Services.prefs.getBoolPref(PREF_SEARCHTERMS), + true, + "Preference should be true if checked." + ); + + // Clean-up. + Services.prefs.clearUserPref(PREF_SEARCHTERMS); + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/* + When loading the search preferences panel, the + showSearchTerms checkbox should be disabled if + the search bar is enabled. +*/ +add_task(async function showSearchTerms_and_searchBar_preference_load() { + // Enable the feature. + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_FEATUREGATE, true], + ["browser.search.widget.inNavBar", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let checkbox = doc.getElementById(CHECKBOX_ID); + Assert.ok( + checkbox.disabled, + "showSearchTerms checkbox should be disabled when search bar is enabled." + ); + + // Clean-up. + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/* + If the search bar is enabled while the search + preferences panel is open, the showSearchTerms + checkbox should not be clickable. +*/ +add_task(async function showSearchTerms_and_searchBar_preference_change() { + // Enable the feature. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, true]], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let checkbox = doc.getElementById(CHECKBOX_ID); + Assert.ok(!checkbox.disabled, "showSearchTerms checkbox should be enabled."); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.widget.inNavBar", true]], + }); + Assert.ok( + checkbox.disabled, + "showSearchTerms checkbox should be disabled when search bar is enabled." + ); + + // Clean-up. + await SpecialPowers.popPrefEnv(); + Assert.ok(!checkbox.disabled, "showSearchTerms checkbox should be enabled."); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js b/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js new file mode 100644 index 0000000000..8235f8dd19 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js @@ -0,0 +1,39 @@ +"use strict"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +add_task(async function test_show_search_term_tooltip_in_subdialog() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let keyword = "organization"; + await runSearchInput(keyword); + + let formAutofillGroupBox = gBrowser.contentDocument.getElementById( + "formAutofillGroupBox" + ); + let savedAddressesButton = + formAutofillGroupBox.querySelector(".accessory-button"); + + info("Clicking saved addresses button to open subdialog"); + savedAddressesButton.click(); + info("Waiting for addresses subdialog to appear"); + await BrowserTestUtils.waitForCondition(() => { + let dialogBox = gBrowser.contentDocument.querySelector(".dialogBox"); + return !!dialogBox; + }); + let tooltip = gBrowser.contentDocument.querySelector(".search-tooltip"); + + is_element_visible( + tooltip, + "Tooltip with search term should be visible in subdialog" + ); + is(tooltip.textContent, keyword, "Tooltip should have correct search term"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js new file mode 100644 index 0000000000..e676de0a6c --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js @@ -0,0 +1,48 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Set Home Page" subdialog. + */ +add_task(async function () { + // Set custom URL so bookmark button will be shown on the page (otherwise it is hidden) + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.startup.homepage", "about:robots"], + ["browser.startup.page", 1], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + // Wait for Activity Stream to add its panels + await BrowserTestUtils.waitForCondition(() => + SpecialPowers.spawn( + gBrowser.selectedTab.linkedBrowser, + [], + () => !!content.document.getElementById("homeContentsGroup") + ) + ); + + await evaluateSearchResults("Set Home Page", "homepageGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Languages" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Choose languages", "languagesGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js new file mode 100644 index 0000000000..6ff05dee29 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js @@ -0,0 +1,36 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Saved Logins" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("sites are stored", "passwordsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Exceptions - Enhanced Tracking Protection" subdialog: + * "You can specify which websites have Enhanced Tracking Protection turned off." #permissions-exceptions-manage-etp-desc + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults( + "Enhanced Tracking Protection turned off", + "trackingGroup" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js new file mode 100644 index 0000000000..45f8774d73 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js @@ -0,0 +1,35 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Allowed Sites - Add-ons Installation" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("allowed to install add-ons", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Certificate Manager" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults( + "identify these certificate authorities", + "certSelection" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js new file mode 100644 index 0000000000..9cfcb51f1b --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js @@ -0,0 +1,39 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Update History" subdialog. + */ +add_task(async function () { + // The updates panel is disabled in MSIX builds. + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + return; + } + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("updates have been installed", "updateApp"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Location Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("location permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js new file mode 100644 index 0000000000..9a4ae09696 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js @@ -0,0 +1,46 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(2); + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Fonts" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + // Oh, Canada: + await evaluateSearchResults("Unified Canadian Syllabary", "fontsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Colors" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Link Colors", "colorsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Exceptions - Saved Logins" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("sites will not be saved", "passwordsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js new file mode 100644 index 0000000000..1091dd2dd4 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js @@ -0,0 +1,35 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Block Lists" subdialog. + */ +add_task(async function () { + async function doTest() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("block online trackers", "trackingGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + await doTest(); +}); + +/** + * Test for searching for the "Allowed Sites - Pop-ups" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("open pop-up windows", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js new file mode 100644 index 0000000000..1c4a923f06 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js @@ -0,0 +1,34 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(2); + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Device Manager" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Security Modules and Devices", "certSelection"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Connection Settings" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Use system proxy settings", "connectionGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js new file mode 100644 index 0000000000..9827e89239 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js @@ -0,0 +1,45 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(2); + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Camera Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("camera permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Microphone Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("microphone permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Notification Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("notification permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js new file mode 100644 index 0000000000..b7ca239358 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js @@ -0,0 +1,45 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Settings - Site Data" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("cookies", ["siteDataGroup", "trackingGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("site data", ["siteDataGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("cache", ["siteDataGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("cross-site", ["trackingGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_within_preferences_1.js b/browser/components/preferences/tests/browser_search_within_preferences_1.js new file mode 100644 index 0000000000..504191ecb5 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_1.js @@ -0,0 +1,344 @@ +"use strict"; +/** + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(6); + +/** + * Tests to see if search bar is being shown when pref is turned on + */ +add_task(async function show_search_bar_when_pref_is_enabled() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + is_element_visible(searchInput, "Search box should be shown"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for "Search Result" panel. + * After it runs a search, it tests if the "Search Results" panel is the only selected category. + * The search is then cleared, it then tests if the "General" panel is the only selected category. + */ +add_task(async function show_search_results_pane_only_then_revert_to_general() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "password"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let categoriesList = gBrowser.contentDocument.getElementById("categories"); + + for (let i = 0; i < categoriesList.childElementCount; i++) { + let child = categoriesList.itemChildren[i]; + is(child.selected, false, "No other panel should be selected"); + } + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + // Checks if back to generalPane + for (let i = 0; i < categoriesList.childElementCount; i++) { + let child = categoriesList.itemChildren[i]; + if (child.id == "category-general") { + is(child.selected, true, "General panel should be selected"); + } else if (child.id) { + is(child.selected, false, "No other panel should be selected"); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for "password" case. When we search "password", it should show the "passwordGroup" + */ +add_task(async function search_for_password_show_passwordGroup() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "password"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane"); + + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if ( + child.id == "passwordsGroup" || + child.id == "weavePrefsDeck" || + child.id == "header-searchResults" || + child.id == "certSelection" || + child.id == "connectionGroup" || + child.id == "dataMigrationGroup" + ) { + is_element_visible(child, "Should be in search results"); + } else if (child.id) { + is_element_hidden(child, "Should not be in search results"); + } + } + + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + let expectedChildren = [ + "paneGeneral", + "startupGroup", + "languagesGroup", + "webAppearanceGroup", + "colorsGroup", + "fontsGroup", + "zoomGroup", + "downloadsGroup", + "applicationsGroup", + "drmGroup", + "browsingGroup", + "performanceGroup", + "connectionGroup", + "generalCategory", + "languageAndAppearanceCategory", + "filesAndApplicationsCategory", + "performanceCategory", + "browsingCategory", + "networkProxyCategory", + "dataMigrationGroup", + "translationsGroup", + ]; + // Only visible for non-MSIX builds + if ( + AppConstants.platform !== "win" || + !Services.sysinfo.getProperty("hasWinPackageId", false) + ) { + expectedChildren.push("updatesCategory"); + expectedChildren.push("updateApp"); + } + // Checks if back to generalPane + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if (expectedChildren.includes(child.id)) { + is_element_visible(child, `Should be in general tab: ${child.id}`); + } else if (child.id) { + is_element_hidden(child, `Should not be in general tab: ${child.id}`); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for if nothing is found + */ +add_task(async function search_with_nothing_found() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let noResultsEl = gBrowser.contentDocument.querySelector( + "#no-results-message" + ); + let sorryMsgQueryEl = gBrowser.contentDocument.getElementById( + "sorry-message-query" + ); + + is_element_hidden(noResultsEl, "Should not be in search results yet"); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "coach"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + is_element_visible(noResultsEl, "Should be in search results"); + is( + sorryMsgQueryEl.textContent, + query, + "sorry-message-query should contain the query" + ); + + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + is_element_hidden(noResultsEl, "Should not be in search results"); + is( + sorryMsgQueryEl.textContent.length, + 0, + "sorry-message-query should be empty" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for if we go back to general tab after search case + */ +add_task(async function exiting_search_reverts_to_general_pane() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let generalPane = gBrowser.contentDocument.getElementById("generalCategory"); + + is_element_hidden(generalPane, "Should not be in general"); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "password"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + // Checks if back to normal + is_element_visible(generalPane, "Should be in generalPane"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for if we go to another tab after searching + */ +add_task(async function changing_tabs_after_searching() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "permission"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + // Search header should be shown for the permissions group + let permissionsSearchHeader = gBrowser.contentDocument.querySelector( + "#permissionsGroup .search-header" + ); + is( + permissionsSearchHeader.hidden, + false, + "Permissions search-header should be visible" + ); + + let privacyCategory = + gBrowser.contentDocument.getElementById("category-privacy"); + privacyCategory.click(); + is(searchInput.value, "", "search input should be empty"); + let categoriesList = gBrowser.contentDocument.getElementById("categories"); + for (let i = 0; i < categoriesList.childElementCount; i++) { + let child = categoriesList.itemChildren[i]; + if (child.id == "category-privacy") { + is(child.selected, true, "Privacy panel should be selected"); + } else if (child.id) { + is(child.selected, false, "No other panel should be selected"); + } + } + + // Search header should now be hidden when viewing the permissions group not through a search + is( + permissionsSearchHeader.hidden, + true, + "Permissions search-header should be hidden" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_within_preferences_2.js b/browser/components/preferences/tests/browser_search_within_preferences_2.js new file mode 100644 index 0000000000..6de068fbe4 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_2.js @@ -0,0 +1,180 @@ +"use strict"; +/** + * This file contains tests for the Preferences search bar. + */ + +/** + * Enabling searching functionality. Will display search bar from this testcase forward. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test that we only search the selected child of a XUL deck. + * When we search "Remove Account", + * it should not show the "Remove Account" button if the Firefox account is not logged in yet. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneSync", { leaveOpen: true }); + + let weavePrefsDeck = + gBrowser.contentDocument.getElementById("weavePrefsDeck"); + is( + weavePrefsDeck.selectedIndex, + 0, + "Should select the #noFxaAccount child node" + ); + + // Performs search. + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "Sync"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane"); + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if (child.id == "header-searchResults" || child.id == "weavePrefsDeck") { + is_element_visible(child, "Should be in search results"); + } else if (child.id) { + is_element_hidden(child, "Should not be in search results"); + } + } + + // Ensure the "Remove Account" button exists in the hidden child of the <xul:deck>. + let unlinkFxaAccount = weavePrefsDeck.children[1].querySelector( + "#unverifiedUnlinkFxaAccount" + ); + is( + unlinkFxaAccount.label, + "Remove Account", + "The Remove Account button should exist" + ); + + // Performs search. + searchInput.focus(); + query = "Remove Account"; + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let noResultsEl = gBrowser.contentDocument.querySelector( + "#no-results-message" + ); + is_element_visible(noResultsEl, "Should be reporting no results"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test that we search using `search-l10n-ids`. + * + * The test uses element `showUpdateHistory` and + * l10n id `language-and-appearance-header` and expects the element + * to be matched on the first word from the l10n id value ("Language" in en-US). + */ +add_task(async function () { + let l10nId = "language-and-appearance-header"; + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // First, lets make sure that the element is not matched without + // `search-l10n-ids`. + { + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + let suhElem = gBrowser.contentDocument.getElementById("showUpdateHistory"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + ok( + !suhElem.getAttribute("search-l10n-ids").includes(l10nId), + "showUpdateHistory element should not contain the l10n id here." + ); + + let query = "Language"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + is_element_hidden( + suhElem, + "showUpdateHistory should not be in search results" + ); + } + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Now, let's add the l10n id to the element and perform the same search again. + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + { + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let suhElem = gBrowser.contentDocument.getElementById("showUpdateHistory"); + suhElem.setAttribute("search-l10n-ids", l10nId); + + let query = "Language"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + is_element_hidden( + suhElem, + "showUpdateHistory should not be in search results" + ); + } else { + is_element_visible( + suhElem, + "showUpdateHistory should be in search results" + ); + } + } + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_within_preferences_command.js b/browser/components/preferences/tests/browser_search_within_preferences_command.js new file mode 100644 index 0000000000..736493b418 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_command.js @@ -0,0 +1,45 @@ +"use strict"; + +/** + * Test for "command" event on search input (when user clicks the x button) + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let generalPane = gBrowser.contentDocument.getElementById("generalCategory"); + + is_element_hidden(generalPane, "Should not be in general"); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "x"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + is_element_hidden(generalPane, "Should not be in generalPane"); + + // Takes search off with "command" + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + searchInput.value = ""; + searchInput.doCommand(); + await searchCompletedPromise; + + // Checks if back to normal + is_element_visible(generalPane, "Should be in generalPane"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_searchsuggestions.js b/browser/components/preferences/tests/browser_searchsuggestions.js new file mode 100644 index 0000000000..f3ce1615ea --- /dev/null +++ b/browser/components/preferences/tests/browser_searchsuggestions.js @@ -0,0 +1,128 @@ +const SUGGEST_PREF_NAME = "browser.search.suggest.enabled"; +const URLBAR_SUGGEST_PREF_NAME = "browser.urlbar.suggest.searches"; +const PRIVATE_PREF_NAME = "browser.search.suggest.enabled.private"; + +let initialUrlbarSuggestValue; +let initialSuggestionsInPrivateValue; + +add_setup(async function () { + const originalSuggest = Services.prefs.getBoolPref(SUGGEST_PREF_NAME); + initialUrlbarSuggestValue = Services.prefs.getBoolPref( + URLBAR_SUGGEST_PREF_NAME + ); + initialSuggestionsInPrivateValue = + Services.prefs.getBoolPref(PRIVATE_PREF_NAME); + + registerCleanupFunction(() => { + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, originalSuggest); + Services.prefs.setBoolPref( + PRIVATE_PREF_NAME, + initialSuggestionsInPrivateValue + ); + }); +}); + +// Open with suggestions enabled +add_task(async function test_suggestions_start_enabled() { + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let urlbarBox = doc.getElementById("urlBarSuggestion"); + let privateBox = doc.getElementById("showSearchSuggestionsPrivateWindows"); + ok(!urlbarBox.disabled, "Should have enabled the urlbar checkbox"); + ok( + !privateBox.disabled, + "Should have enabled the private mode suggestions checkbox" + ); + is( + urlbarBox.checked, + initialUrlbarSuggestValue, + "Should have the correct value for the urlbar checkbox" + ); + is( + privateBox.checked, + initialSuggestionsInPrivateValue, + "Should have the correct value for the private mode suggestions checkbox" + ); + + async function toggleElement(id, prefName, element, initialValue, desc) { + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${id}`, + {}, + gBrowser.selectedBrowser + ); + is( + element.checked, + !initialValue, + `Should have flipped the ${desc} checkbox` + ); + let prefValue = Services.prefs.getBoolPref(prefName); + is( + prefValue, + !initialValue, + `Should have updated the ${desc} preference value` + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${id}`, + {}, + gBrowser.selectedBrowser + ); + is( + element.checked, + initialValue, + `Should have flipped the ${desc} checkbox back to the original value` + ); + prefValue = Services.prefs.getBoolPref(prefName); + is( + prefValue, + initialValue, + `Should have updated the ${desc} preference back to the original value` + ); + } + + await toggleElement( + "urlBarSuggestion", + URLBAR_SUGGEST_PREF_NAME, + urlbarBox, + initialUrlbarSuggestValue, + "urlbar" + ); + await toggleElement( + "showSearchSuggestionsPrivateWindows", + PRIVATE_PREF_NAME, + privateBox, + initialSuggestionsInPrivateValue, + "private suggestion" + ); + + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false); + ok(!urlbarBox.checked, "Should have unchecked the urlbar box"); + ok(urlbarBox.disabled, "Should have disabled the urlbar box"); + ok(!privateBox.checked, "Should have unchecked the private suggestions box"); + ok(privateBox.disabled, "Should have disabled the private suggestions box"); + + gBrowser.removeCurrentTab(); +}); + +// Open with suggestions disabled +add_task(async function test_suggestions_start_disabled() { + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let urlbarBox = doc.getElementById("urlBarSuggestion"); + ok(urlbarBox.disabled, "Should have the urlbar box disabled"); + let privateBox = doc.getElementById("showSearchSuggestionsPrivateWindows"); + ok(privateBox.disabled, "Should have the private suggestions box disabled"); + + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true); + + ok(!urlbarBox.disabled, "Should have enabled the urlbar box"); + ok(!privateBox.disabled, "Should have enabled the private suggestions box"); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_security-1.js b/browser/components/preferences/tests/browser_security-1.js new file mode 100644 index 0000000000..80f3e6902b --- /dev/null +++ b/browser/components/preferences/tests/browser_security-1.js @@ -0,0 +1,106 @@ +const PREFS = [ + "browser.safebrowsing.phishing.enabled", + "browser.safebrowsing.malware.enabled", + + "browser.safebrowsing.downloads.enabled", + + "browser.safebrowsing.downloads.remote.block_potentially_unwanted", + "browser.safebrowsing.downloads.remote.block_uncommon", +]; + +let originals = PREFS.map(pref => [pref, Services.prefs.getBoolPref(pref)]); +let originalMalwareTable = Services.prefs.getCharPref( + "urlclassifier.malwareTable" +); +registerCleanupFunction(function () { + originals.forEach(([pref, val]) => Services.prefs.setBoolPref(pref, val)); + Services.prefs.setCharPref( + "urlclassifier.malwareTable", + originalMalwareTable + ); +}); + +// This test only opens the Preferences once, and then reloads the page +// each time that it wants to test various preference combinations. We +// only use one tab (instead of opening/closing for each test) for all +// to help improve test times on debug builds. +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +// test the safebrowsing preference +add_task(async function () { + async function checkPrefSwitch(val1, val2) { + Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled", val1); + Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", val2); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById("enableSafeBrowsing"); + let blockDownloads = doc.getElementById("blockDownloads"); + let blockUncommon = doc.getElementById("blockUncommonUnwanted"); + let checked = checkbox.checked; + is( + blockDownloads.hasAttribute("disabled"), + !checked, + "block downloads checkbox is set correctly" + ); + + is( + checked, + val1 && val2, + "safebrowsing preference is initialized correctly" + ); + // should be disabled when checked is false (= pref is turned off) + is( + blockUncommon.hasAttribute("disabled"), + !checked, + "block uncommon checkbox is set correctly" + ); + + // scroll the checkbox into the viewport and click checkbox + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + // check that both settings are now turned on or off + is( + Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled"), + !checked, + "safebrowsing.enabled is set correctly" + ); + is( + Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled"), + !checked, + "safebrowsing.malware.enabled is set correctly" + ); + + // check if the other checkboxes have updated + checked = checkbox.checked; + if (blockDownloads) { + is( + blockDownloads.hasAttribute("disabled"), + !checked, + "block downloads checkbox is set correctly" + ); + is( + blockUncommon.hasAttribute("disabled"), + !checked || !blockDownloads.checked, + "block uncommon checkbox is set correctly" + ); + } + } + + await checkPrefSwitch(true, true); + await checkPrefSwitch(false, true); + await checkPrefSwitch(true, false); + await checkPrefSwitch(false, false); +}); diff --git a/browser/components/preferences/tests/browser_security-2.js b/browser/components/preferences/tests/browser_security-2.js new file mode 100644 index 0000000000..88be366810 --- /dev/null +++ b/browser/components/preferences/tests/browser_security-2.js @@ -0,0 +1,177 @@ +const PREFS = [ + "browser.safebrowsing.phishing.enabled", + "browser.safebrowsing.malware.enabled", + + "browser.safebrowsing.downloads.enabled", + + "browser.safebrowsing.downloads.remote.block_potentially_unwanted", + "browser.safebrowsing.downloads.remote.block_uncommon", +]; + +let originals = PREFS.map(pref => [pref, Services.prefs.getBoolPref(pref)]); +let originalMalwareTable = Services.prefs.getCharPref( + "urlclassifier.malwareTable" +); +registerCleanupFunction(function () { + originals.forEach(([pref, val]) => Services.prefs.setBoolPref(pref, val)); + Services.prefs.setCharPref( + "urlclassifier.malwareTable", + originalMalwareTable + ); +}); + +// This test only opens the Preferences once, and then reloads the page +// each time that it wants to test various preference combinations. We +// only use one tab (instead of opening/closing for each test) for all +// to help improve test times on debug builds. +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +// test the download protection preference +add_task(async function () { + async function checkPrefSwitch(val) { + Services.prefs.setBoolPref("browser.safebrowsing.downloads.enabled", val); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById("blockDownloads"); + + let blockUncommon = doc.getElementById("blockUncommonUnwanted"); + let checked = checkbox.checked; + is(checked, val, "downloads preference is initialized correctly"); + // should be disabled when val is false (= pref is turned off) + is( + blockUncommon.hasAttribute("disabled"), + !val, + "block uncommon checkbox is set correctly" + ); + + // scroll the checkbox into view, otherwise the synthesizeMouseAtCenter will be ignored, and click it + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + // check that setting is now turned on or off + is( + Services.prefs.getBoolPref("browser.safebrowsing.downloads.enabled"), + !checked, + "safebrowsing.downloads preference is set correctly" + ); + + // check if the uncommon warning checkbox has updated + is( + blockUncommon.hasAttribute("disabled"), + val, + "block uncommon checkbox is set correctly" + ); + } + + await checkPrefSwitch(true); + await checkPrefSwitch(false); +}); + +requestLongerTimeout(2); +// test the unwanted/uncommon software warning preference +add_task(async function () { + async function checkPrefSwitch(val1, val2, isV2) { + Services.prefs.setBoolPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted", + val1 + ); + Services.prefs.setBoolPref( + "browser.safebrowsing.downloads.remote.block_uncommon", + val2 + ); + let testMalwareTable = "goog-malware-" + (isV2 ? "shavar" : "proto"); + testMalwareTable += ",test-malware-simple"; + if (val1 && val2) { + testMalwareTable += ",goog-unwanted-" + (isV2 ? "shavar" : "proto"); + testMalwareTable += ",moztest-unwanted-simple"; + } + Services.prefs.setCharPref("urlclassifier.malwareTable", testMalwareTable); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById("blockUncommonUnwanted"); + let checked = checkbox.checked; + is( + checked, + val1 && val2, + "unwanted/uncommon preference is initialized correctly" + ); + + // scroll the checkbox into view, otherwise the synthesizeMouseAtCenter will be ignored, and click it + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + // check that both settings are now turned on or off + is( + Services.prefs.getBoolPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted" + ), + !checked, + "block_potentially_unwanted is set correctly" + ); + is( + Services.prefs.getBoolPref( + "browser.safebrowsing.downloads.remote.block_uncommon" + ), + !checked, + "block_uncommon is set correctly" + ); + + // when the preference is on, the malware table should include these ids + let malwareTable = Services.prefs + .getCharPref("urlclassifier.malwareTable") + .split(","); + if (isV2) { + is( + malwareTable.includes("goog-unwanted-shavar"), + !checked, + "malware table doesn't include goog-unwanted-shavar" + ); + } else { + is( + malwareTable.includes("goog-unwanted-proto"), + !checked, + "malware table doesn't include goog-unwanted-proto" + ); + } + is( + malwareTable.includes("moztest-unwanted-simple"), + !checked, + "malware table doesn't include moztest-unwanted-simple" + ); + let sortedMalware = malwareTable.slice(0); + sortedMalware.sort(); + Assert.deepEqual( + malwareTable, + sortedMalware, + "malware table has been sorted" + ); + } + + await checkPrefSwitch(true, true, false); + await checkPrefSwitch(false, true, false); + await checkPrefSwitch(true, false, false); + await checkPrefSwitch(false, false, false); + await checkPrefSwitch(true, true, true); + await checkPrefSwitch(false, true, true); + await checkPrefSwitch(true, false, true); + await checkPrefSwitch(false, false, true); +}); diff --git a/browser/components/preferences/tests/browser_security-3.js b/browser/components/preferences/tests/browser_security-3.js new file mode 100644 index 0000000000..75872d6629 --- /dev/null +++ b/browser/components/preferences/tests/browser_security-3.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + registerCleanupFunction(async function () { + Services.prefs.unlockPref("browser.safebrowsing.phishing.enabled"); + Services.prefs.unlockPref("browser.safebrowsing.malware.enabled"); + Services.prefs.unlockPref("browser.safebrowsing.downloads.enabled"); + Services.prefs.unlockPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted" + ); + Services.prefs.unlockPref( + "browser.safebrowsing.downloads.remote.block_uncommon" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +// This test just reloads the preferences page for the various tests. +add_task(async function () { + Services.prefs.lockPref("browser.safebrowsing.phishing.enabled"); + Services.prefs.lockPref("browser.safebrowsing.malware.enabled"); + Services.prefs.lockPref("browser.safebrowsing.downloads.enabled"); + Services.prefs.lockPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted" + ); + Services.prefs.lockPref( + "browser.safebrowsing.downloads.remote.block_uncommon" + ); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + is( + doc.getElementById("enableSafeBrowsing").disabled, + true, + "Safe browsing should be disabled" + ); + is( + doc.getElementById("blockDownloads").disabled, + true, + "Block downloads should be disabled" + ); + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + Services.prefs.unlockPref("browser.safebrowsing.phishing.enabled"); + Services.prefs.unlockPref("browser.safebrowsing.malware.enabled"); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + doc = gBrowser.selectedBrowser.contentDocument; + + let checkbox = doc.getElementById("enableSafeBrowsing"); + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockDownloads").disabled, + true, + "Block downloads should be disabled" + ); + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockDownloads").disabled, + true, + "Block downloads should be disabled" + ); + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + Services.prefs.unlockPref("browser.safebrowsing.downloads.enabled"); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + doc = gBrowser.selectedBrowser.contentDocument; + + checkbox = doc.getElementById("blockDownloads"); + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); +}); diff --git a/browser/components/preferences/tests/browser_site_login_exceptions.js b/browser/components/preferences/tests/browser_site_login_exceptions.js new file mode 100644 index 0000000000..508232b234 --- /dev/null +++ b/browser/components/preferences/tests/browser_site_login_exceptions.js @@ -0,0 +1,101 @@ +"use strict"; +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +var exceptionsDialog; + +add_task(async function openLoginExceptionsSubDialog() { + // ensure rememberSignons is off for this test; + ok( + !Services.prefs.getBoolPref("signon.rememberSignons"), + "Check initial value of signon.rememberSignons pref" + ); + + // Undo the save password change. + registerCleanupFunction(async function () { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + if (savePasswordCheckBox.checked) { + savePasswordCheckBox.click(); + } + }); + + gBrowser.removeCurrentTab(); + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + Assert.ok( + !savePasswordCheckBox.checked, + "Save Password CheckBox should be unchecked by default" + ); + savePasswordCheckBox.click(); + + let loginExceptionsButton = doc.getElementById("passwordExceptions"); + loginExceptionsButton.click(); + }); + + exceptionsDialog = await dialogOpened; +}); + +add_task(async function addALoginException() { + let doc = exceptionsDialog.document; + + let richlistbox = doc.getElementById("permissionsBox"); + Assert.equal(richlistbox.itemCount, 0, "Row count should initially be 0"); + + let inputBox = doc.getElementById("url"); + inputBox.focus(); + + EventUtils.sendString("www.example.com", exceptionsDialog); + + let btnBlock = doc.getElementById("btnBlock"); + btnBlock.click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 2); + + let expectedResult = ["http://www.example.com", "https://www.example.com"]; + for (let website of expectedResult) { + let elements = richlistbox.getElementsByAttribute("origin", website); + is(elements.length, 1, "It should find only one coincidence"); + } +}); + +add_task(async function deleteALoginException() { + let doc = exceptionsDialog.document; + + let richlistbox = doc.getElementById("permissionsBox"); + let currentItems = 2; + Assert.equal( + richlistbox.itemCount, + currentItems, + `Row count should initially be ${currentItems}` + ); + richlistbox.focus(); + + while (richlistbox.itemCount) { + richlistbox.selectedIndex = 0; + + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_Backspace"); + } else { + EventUtils.synthesizeKey("KEY_Delete"); + } + + currentItems -= 1; + + await TestUtils.waitForCondition( + () => richlistbox.itemCount == currentItems + ); + is_element_visible( + content.gSubDialog._dialogs[0]._box, + "Subdialog is visible after deleting an element" + ); + } +}); diff --git a/browser/components/preferences/tests/browser_site_login_exceptions_policy.js b/browser/components/preferences/tests/browser_site_login_exceptions_policy.js new file mode 100644 index 0000000000..10f5039c6f --- /dev/null +++ b/browser/components/preferences/tests/browser_site_login_exceptions_policy.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +var exceptionsDialog; + +add_task(async function openLoginExceptionsSubDialog() { + // ensure rememberSignons is off for this test; + ok( + !Services.prefs.getBoolPref("signon.rememberSignons"), + "Check initial value of signon.rememberSignons pref" + ); + + // Undo the save password change. + registerCleanupFunction(async function () { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + if (savePasswordCheckBox.checked) { + savePasswordCheckBox.click(); + } + }); + + gBrowser.removeCurrentTab(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); + }); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + PasswordManagerExceptions: ["https://pwexception.example.com"], + }, + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + savePasswordCheckBox.click(); + + let loginExceptionsButton = doc.getElementById("passwordExceptions"); + loginExceptionsButton.click(); + }); + + exceptionsDialog = await dialogOpened; + + let doc = exceptionsDialog.document; + + let richlistbox = doc.getElementById("permissionsBox"); + Assert.equal(richlistbox.itemCount, 1, `Row count should initially be 1`); + + richlistbox.focus(); + richlistbox.selectedIndex = 0; + Assert.ok(doc.getElementById("removePermission").disabled); +}); diff --git a/browser/components/preferences/tests/browser_spotlight.js b/browser/components/preferences/tests/browser_spotlight.js new file mode 100644 index 0000000000..4a1aae7ec1 --- /dev/null +++ b/browser/components/preferences/tests/browser_spotlight.js @@ -0,0 +1,72 @@ +add_task(async function test_openPreferences_spotlight() { + for (let [arg, expectedPane, expectedHash, expectedSubcategory] of [ + ["privacy-reports", "panePrivacy", "#privacy", "reports"], + ["privacy-address-autofill", "panePrivacy", "#privacy", "address-autofill"], + [ + "privacy-credit-card-autofill", + "panePrivacy", + "#privacy", + "credit-card-autofill", + ], + ["privacy-form-autofill", "panePrivacy", "#privacy", "form-autofill"], + ["privacy-logins", "panePrivacy", "#privacy", "logins"], + [ + "privacy-trackingprotection", + "panePrivacy", + "#privacy", + "trackingprotection", + ], + [ + "privacy-permissions-block-popups", + "panePrivacy", + "#privacy", + "permissions-block-popups", + ], + ]) { + if ( + arg == "privacy-credit-card-autofill" && + Services.prefs.getCharPref( + "extensions.formautofill.creditCards.supported" + ) == "off" + ) { + continue; + } + if ( + arg == "privacy-address-autofill" && + Services.prefs.getCharPref( + "extensions.formautofill.addresses.supported" + ) == "off" + ) { + continue; + } + + let prefs = await openPreferencesViaOpenPreferencesAPI(arg, { + leaveOpen: true, + }); + is(prefs.selectedPane, expectedPane, "The right pane is selected"); + let doc = gBrowser.contentDocument; + is( + doc.location.hash, + expectedHash, + "The subcategory should be removed from the URI" + ); + await TestUtils.waitForCondition( + () => doc.querySelector(".spotlight"), + "Wait for the spotlight" + ); + is( + doc.querySelector(".spotlight").getAttribute("data-subcategory"), + expectedSubcategory, + "The right subcategory is spotlighted" + ); + + doc.defaultView.spotlight(null); + is( + doc.querySelector(".spotlight"), + null, + "The spotlighted section is cleared" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); diff --git a/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js b/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js new file mode 100644 index 0000000000..73d12a1bf9 --- /dev/null +++ b/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js @@ -0,0 +1,124 @@ +"use strict"; + +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIOR_PBM_PREF = "network.cookie.cookieBehavior.pbmode"; + +const CB_STRICT_FEATURES_PREF = "browser.contentblocking.features.strict"; +const FPI_PREF = "privacy.firstparty.isolate"; + +async function testCookieBlockingInfoStandard( + cookieBehavior, + cookieBehaviorPBM, + isShown +) { + let defaults = Services.prefs.getDefaultBranch(""); + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, cookieBehavior); + defaults.setIntPref(COOKIE_BEHAVIOR_PBM_PREF, cookieBehaviorPBM); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + + // Select standard mode. + let standardRadioOption = doc.getElementById("standardRadio"); + standardRadioOption.click(); + + // Check the cookie blocking info for private windows for standard mode. + let elts = doc.querySelectorAll( + "#contentBlockingOptionStandard .extra-information-label.all-third-party-cookies-private-windows-option" + ); + for (let elt of elts) { + is( + elt.hidden, + !isShown, + `The visibility of cookie blocking info for standard mode is correct` + ); + } + + gBrowser.removeCurrentTab(); +} + +async function testCookieBlockingInfoStrict( + contentBlockingStrictFeatures, + isShown +) { + await SpecialPowers.pushPrefEnv({ + set: [ + [CB_STRICT_FEATURES_PREF, contentBlockingStrictFeatures], + [FPI_PREF, false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + + // Select strict mode. + let strictRadioOption = doc.getElementById("strictRadio"); + strictRadioOption.click(); + + // Check the cookie blocking info for private windows for strict mode. + let elts = doc.querySelectorAll( + "#contentBlockingOptionStrict .extra-information-label.all-third-party-cookies-private-windows-option" + ); + for (let elt of elts) { + is( + elt.hidden, + !isShown, + `The cookie blocking info is hidden for strict mode` + ); + } + + gBrowser.removeCurrentTab(); +} + +add_task(async function runTests() { + await SpecialPowers.pushPrefEnv({ + set: [[FPI_PREF, false]], + }); + + let defaults = Services.prefs.getDefaultBranch(""); + let originalCookieBehavior = defaults.getIntPref(COOKIE_BEHAVIOR_PREF); + let originalCookieBehaviorPBM = defaults.getIntPref(COOKIE_BEHAVIOR_PBM_PREF); + + // Test if the cookie blocking info for state partitioning in PBM is + // shown in standard mode if the regular cookieBehavior is + // BEHAVIOR_REJECT_TRACKER and the private cookieBehavior is + // BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + await testCookieBlockingInfoStandard( + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + true + ); + + // Test if the cookie blocking info is hidden in standard mode if both + // cookieBehaviors are the same. + await testCookieBlockingInfoStandard( + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + false + ); + + // Test if the cookie blocking info is hidden for strict mode if + // cookieBehaviors both are BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN in + // the strict feature value. + await testCookieBlockingInfoStrict( + "tp,tpPrivate,cookieBehavior5,cookieBehaviorPBM5,cm,fp,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp", + false + ); + + // Test if the cookie blocking info is shown for strict mode if the regular + // cookieBehavior is BEHAVIOR_REJECT_TRACKER and the private cookieBehavior is + // BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + await testCookieBlockingInfoStrict( + "tp,tpPrivate,cookieBehavior4,cookieBehaviorPBM5,cm,fp,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp", + true + ); + + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, originalCookieBehavior); + defaults.setIntPref(COOKIE_BEHAVIOR_PBM_PREF, originalCookieBehaviorPBM); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_statePartitioning_strings.js b/browser/components/preferences/tests/browser_statePartitioning_strings.js new file mode 100644 index 0000000000..aed7e26977 --- /dev/null +++ b/browser/components/preferences/tests/browser_statePartitioning_strings.js @@ -0,0 +1,79 @@ +"use strict"; + +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const CB_STRICT_FEATURES_PREF = "browser.contentblocking.features.strict"; +const CB_STRICT_FEATURES_VALUE = "tp,tpPrivate,cookieBehavior5,cm,fp,stp,lvl2"; +const FPI_PREF = "privacy.firstparty.isolate"; +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIOR_VALUE = 5; + +async function testStrings() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + + // Check the cookie blocking info strings + let elts = doc.querySelectorAll( + ".extra-information-label.cross-site-cookies-option" + ); + for (let elt of elts) { + ok(!elt.hidden, "The new cross-site cookies info label is visible"); + } + + // Check the learn more strings + elts = doc.querySelectorAll( + ".tail-with-learn-more.content-blocking-warning-description" + ); + for (let elt of elts) { + let id = doc.l10n.getAttributes(elt).id; + is( + id, + "content-blocking-and-isolating-etp-warning-description-2", + "The correct warning description string is in use" + ); + } + + // Check the cookie blocking mode menu option string + let elt = doc.querySelector("#isolateCookiesSocialMedia"); + let id = doc.l10n.getAttributes(elt).id; + is( + id, + "sitedata-option-block-cross-site-cookies", + "The correct string is in use for the cookie blocking option" + ); + + // Check the FPI warning is hidden with FPI off + let warningElt = doc.getElementById("fpiIncompatibilityWarning"); + is(warningElt.hidden, true, "The FPI warning is hidden"); + + gBrowser.removeCurrentTab(); + + // Check the FPI warning is shown only if MVP UI is enabled when FPI is on + await SpecialPowers.pushPrefEnv({ set: [[FPI_PREF, true]] }); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + doc = gBrowser.contentDocument; + warningElt = doc.getElementById("fpiIncompatibilityWarning"); + ok(!warningElt.hidden, `The FPI warning is visible`); + await SpecialPowers.popPrefEnv(); + + gBrowser.removeCurrentTab(); +} + +add_task(async function runTests() { + await SpecialPowers.pushPrefEnv({ + set: [ + [CB_STRICT_FEATURES_PREF, CB_STRICT_FEATURES_VALUE], + [FPI_PREF, false], + ], + }); + let defaults = Services.prefs.getDefaultBranch(""); + let originalCookieBehavior = defaults.getIntPref(COOKIE_BEHAVIOR_PREF); + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIOR_VALUE); + registerCleanupFunction(() => { + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, originalCookieBehavior); + }); + + await testStrings(); +}); diff --git a/browser/components/preferences/tests/browser_subdialogs.js b/browser/components/preferences/tests/browser_subdialogs.js new file mode 100644 index 0000000000..9342575886 --- /dev/null +++ b/browser/components/preferences/tests/browser_subdialogs.js @@ -0,0 +1,639 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the sub-dialog infrastructure, not for actual sub-dialog functionality. + */ + +const gDialogURL = getRootDirectory(gTestPath) + "subdialog.xhtml"; +const gDialogURL2 = getRootDirectory(gTestPath) + "subdialog2.xhtml"; + +function open_subdialog_and_test_generic_start_state( + browser, + domcontentloadedFn, + url = gDialogURL +) { + let domcontentloadedFnStr = domcontentloadedFn + ? "(" + domcontentloadedFn.toString() + ")()" + : ""; + return SpecialPowers.spawn( + browser, + [{ url, domcontentloadedFnStr }], + async function (args) { + let rv = { acceptCount: 0 }; + let win = content.window; + content.gSubDialog.open(args.url, undefined, rv); + let subdialog = content.gSubDialog._topDialog; + + info("waiting for subdialog DOMFrameContentLoaded"); + let dialogOpenPromise; + await new Promise(resolve => { + win.addEventListener( + "DOMFrameContentLoaded", + function frameContentLoaded(ev) { + // We can get events for loads in other frames, so we have to filter + // those out. + if (ev.target != subdialog._frame) { + return; + } + win.removeEventListener( + "DOMFrameContentLoaded", + frameContentLoaded + ); + dialogOpenPromise = ContentTaskUtils.waitForEvent( + subdialog._overlay, + "dialogopen" + ); + resolve(); + }, + { capture: true } + ); + }); + let result; + if (args.domcontentloadedFnStr) { + // eslint-disable-next-line no-eval + result = eval(args.domcontentloadedFnStr); + } + + info("waiting for subdialog load"); + await dialogOpenPromise; + info("subdialog window is loaded"); + + let expectedStyleSheetURLs = subdialog._injectedStyleSheets.slice(0); + for (let styleSheet of subdialog._frame.contentDocument.styleSheets) { + let index = expectedStyleSheetURLs.indexOf(styleSheet.href); + if (index >= 0) { + expectedStyleSheetURLs.splice(index, 1); + } + } + + Assert.ok( + !!subdialog._frame.contentWindow, + "The dialog should be non-null" + ); + Assert.notEqual( + subdialog._frame.contentWindow.location.toString(), + "about:blank", + "Subdialog URL should not be about:blank" + ); + Assert.equal( + win.getComputedStyle(subdialog._overlay).visibility, + "visible", + "Overlay should be visible" + ); + Assert.equal( + expectedStyleSheetURLs.length, + 0, + "No stylesheets that were expected are missing" + ); + return result; + } + ); +} + +async function close_subdialog_and_test_generic_end_state( + browser, + closingFn, + closingButton, + acceptCount, + options +) { + let getDialogsCount = () => { + return SpecialPowers.spawn( + browser, + [], + () => content.window.gSubDialog._dialogs.length + ); + }; + let getStackChildrenCount = () => { + return SpecialPowers.spawn( + browser, + [], + () => content.window.gSubDialog._dialogStack.children.length + ); + }; + let dialogclosingPromise = SpecialPowers.spawn( + browser, + [{ closingButton, acceptCount }], + async function (expectations) { + let win = content.window; + let subdialog = win.gSubDialog._topDialog; + let frame = subdialog._frame; + + let frameWinUnload = ContentTaskUtils.waitForEvent( + frame.contentWindow, + "unload", + true + ); + + let actualAcceptCount; + info("waiting for dialogclosing"); + info("URI " + frame.currentURI?.spec); + let closingEvent = await ContentTaskUtils.waitForEvent( + frame.contentWindow, + "dialogclosing", + true, + () => { + actualAcceptCount = frame.contentWindow?.arguments[0].acceptCount; + return true; + } + ); + + info("Waiting for subdialog unload"); + await frameWinUnload; + + let contentClosingButton = closingEvent.detail.button; + + Assert.notEqual( + win.getComputedStyle(subdialog._overlay).visibility, + "visible", + "overlay is not visible" + ); + Assert.equal( + frame.getAttribute("style"), + "", + "inline styles should be cleared" + ); + Assert.equal( + contentClosingButton, + expectations.closingButton, + "closing event should indicate button was '" + + expectations.closingButton + + "'" + ); + Assert.equal( + actualAcceptCount, + expectations.acceptCount, + "should be 1 if accepted, 0 if canceled, undefined if closed w/out button" + ); + } + ); + let initialDialogsCount = await getDialogsCount(); + let initialStackChildrenCount = await getStackChildrenCount(); + if (options && options.runClosingFnOutsideOfContentTask) { + await closingFn(); + } else { + SpecialPowers.spawn(browser, [], closingFn); + } + + await dialogclosingPromise; + let endDialogsCount = await getDialogsCount(); + let endStackChildrenCount = await getStackChildrenCount(); + Assert.equal( + initialDialogsCount - 1, + endDialogsCount, + "dialog count should decrease by 1" + ); + Assert.equal( + initialStackChildrenCount - 1, + endStackChildrenCount, + "stack children count should decrease by 1" + ); +} + +let tab; + +add_task(async function test_initialize() { + tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences" + ); +}); + +add_task( + async function check_titlebar_focus_returnval_titlechanges_accepting() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + let domtitlechangedPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "DOMTitleChanged" + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let dialog = content.window.gSubDialog._topDialog; + let dialogWin = dialog._frame.contentWindow; + let dialogTitleElement = dialog._titleElement; + Assert.equal( + dialogTitleElement.textContent, + "Sample sub-dialog", + "Title should be correct initially" + ); + Assert.equal( + dialogWin.document.activeElement.value, + "Default text", + "Textbox with correct text is focused" + ); + dialogWin.document.title = "Updated title"; + }); + + info("waiting for DOMTitleChanged event"); + await domtitlechangedPromise; + + SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let dialogTitleElement = + content.window.gSubDialog._topDialog._titleElement; + Assert.equal( + dialogTitleElement.textContent, + "Updated title", + "subdialog should have updated title" + ); + }); + + // Accept the dialog + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); + } +); + +add_task(async function check_canceling_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .cancelDialog(); + }, + "cancel", + 0 + ); +}); + +add_task(async function check_reopening_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + info("opening another dialog which will close the first"); + await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + "", + gDialogURL2 + ); + + SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let win = content.window; + let dialogs = win.gSubDialog._dialogs; + let lowerDialog = dialogs[0]; + let topDialog = dialogs[1]; + Assert.equal(dialogs.length, 2, "There should be two visible dialogs"); + Assert.equal( + win.getComputedStyle(topDialog._overlay).visibility, + "visible", + "The top dialog should be visible" + ); + Assert.equal( + win.getComputedStyle(lowerDialog._overlay).visibility, + "visible", + "The lower dialog should be visible" + ); + Assert.equal( + win.getComputedStyle(topDialog._overlay).backgroundColor, + "rgba(0, 0, 0, 0.5)", + "The top dialog should have a semi-transparent overlay" + ); + Assert.equal( + win.getComputedStyle(lowerDialog._overlay).backgroundColor, + "rgba(0, 0, 0, 0)", + "The lower dialog should not have an overlay" + ); + }); + + info("closing two dialogs"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); +}); + +add_task(async function check_opening_while_closing() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + info("closing"); + content.window.gSubDialog._topDialog.close(); + info("reopening immediately after calling .close()"); + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); +}); + +add_task(async function window_close_on_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.close(); + }, + null, + 0 + ); +}); + +add_task(async function click_close_button_on_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + return BrowserTestUtils.synthesizeMouseAtCenter( + ".dialogClose", + {}, + tab.linkedBrowser + ); + }, + null, + 0, + { runClosingFnOutsideOfContentTask: true } + ); +}); + +add_task(async function background_click_should_close_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + // Clicking on an inactive part of dialog itself should not close the dialog. + // Click the dialog title bar here to make sure nothing happens. + info("clicking the dialog title bar"); + BrowserTestUtils.synthesizeMouseAtCenter( + ".dialogTitle", + {}, + tab.linkedBrowser + ); + + // Close the dialog by clicking on the overlay background. Simulate a click + // at point (2,2) instead of (0,0) so we are sure we're clicking on the + // overlay background instead of some boundary condition that a real user + // would never click. + info("clicking the overlay background"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + return BrowserTestUtils.synthesizeMouseAtPoint( + 2, + 2, + {}, + tab.linkedBrowser + ); + }, + null, + 0, + { runClosingFnOutsideOfContentTask: true } + ); +}); + +add_task(async function escape_should_close_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + return BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, tab.linkedBrowser); + }, + "cancel", + 0, + { runClosingFnOutsideOfContentTask: true } + ); +}); + +add_task(async function correct_width_and_height_should_be_used_for_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + function fuzzyEqual(value, expectedValue, fuzz, msg) { + Assert.greaterOrEqual(expectedValue + fuzz, value, msg); + Assert.lessOrEqual(expectedValue - fuzz, value, msg); + } + let topDialog = content.gSubDialog._topDialog; + let frameStyle = content.getComputedStyle(topDialog._frame); + let dialogStyle = topDialog.frameContentWindow.getComputedStyle( + topDialog.frameContentWindow.document.documentElement + ); + let fontSize = parseFloat(dialogStyle.fontSize); + let height = parseFloat(frameStyle.height); + let width = parseFloat(frameStyle.width); + + fuzzyEqual( + width, + fontSize * 32, + 2, + "Width should be set on the frame from the dialog" + ); + fuzzyEqual( + height, + fontSize * 5, + 2, + "Height should be set on the frame from the dialog" + ); + }); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.close(); + }, + null, + 0 + ); +}); + +add_task( + async function wrapped_text_in_dialog_should_have_expected_scrollHeight() { + let oldHeight = await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + function domcontentloadedFn() { + let frame = content.window.gSubDialog._topDialog._frame; + let doc = frame.contentDocument; + let scrollHeight = doc.documentElement.scrollHeight; + doc.documentElement.style.removeProperty("height"); + doc.getElementById("desc").textContent = ` + Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque + laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas + sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione + laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas + sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione + laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas + sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione + voluptatem sequi nesciunt.`; + return scrollHeight; + } + ); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [oldHeight], + async function (contentOldHeight) { + function fuzzyEqual(value, expectedValue, fuzz, msg) { + Assert.greaterOrEqual(expectedValue + fuzz, value, msg); + Assert.lessOrEqual(expectedValue - fuzz, value, msg); + } + let topDialog = content.gSubDialog._topDialog; + let frame = topDialog._frame; + let frameStyle = content.getComputedStyle(frame); + let docEl = frame.contentDocument.documentElement; + let dialogStyle = topDialog.frameContentWindow.getComputedStyle(docEl); + let fontSize = parseFloat(dialogStyle.fontSize); + let height = parseFloat(frameStyle.height); + let width = parseFloat(frameStyle.width); + + fuzzyEqual( + width, + 32 * fontSize, + 2, + "Width should be set on the frame from the dialog" + ); + Assert.ok( + docEl.scrollHeight > contentOldHeight, + "Content height increased (from " + + contentOldHeight + + " to " + + docEl.scrollHeight + + ")." + ); + fuzzyEqual( + height, + docEl.scrollHeight, + 2, + "Height on the frame should be higher now. " + + "This test may fail on certain screen resoluition. " + + "See bug 1420576 and bug 1205717." + ); + } + ); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); + }, + null, + 0 + ); + } +); + +add_task(async function dialog_too_tall_should_get_reduced_in_height() { + await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + function domcontentloadedFn() { + let frame = content.window.gSubDialog._topDialog._frame; + frame.contentDocument.documentElement.style.height = "100000px"; + } + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + function fuzzyEqual(value, expectedValue, fuzz, msg) { + Assert.greaterOrEqual(expectedValue + fuzz, value, msg); + Assert.lessOrEqual(expectedValue - fuzz, value, msg); + } + let topDialog = content.gSubDialog._topDialog; + let frame = topDialog._frame; + let frameStyle = content.getComputedStyle(frame); + let dialogStyle = topDialog.frameContentWindow.getComputedStyle( + frame.contentDocument.documentElement + ); + let fontSize = parseFloat(dialogStyle.fontSize); + let height = parseFloat(frameStyle.height); + let width = parseFloat(frameStyle.width); + fuzzyEqual( + width, + 32 * fontSize, + 2, + "Width should be set on the frame from the dialog" + ); + Assert.less( + height, + content.window.innerHeight, + "Height on the frame should be smaller than window's innerHeight" + ); + }); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); + }, + null, + 0 + ); +}); + +add_task( + async function scrollWidth_and_scrollHeight_from_subdialog_should_size_the_browser() { + await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + function domcontentloadedFn() { + let frame = content.window.gSubDialog._topDialog._frame; + frame.contentDocument.documentElement.style.removeProperty("height"); + frame.contentDocument.documentElement.style.removeProperty("width"); + } + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let frame = content.window.gSubDialog._topDialog._frame; + Assert.ok( + frame.style.width.endsWith("px"), + "Width (" + + frame.style.width + + ") should be set to a px value of the scrollWidth from the dialog" + ); + let cs = content.getComputedStyle(frame); + Assert.stringMatches( + cs.getPropertyValue("--subdialog-inner-height"), + /px$/, + "Height (" + + cs.getPropertyValue("--subdialog-inner-height") + + ") should be set to a px value of the scrollHeight from the dialog" + ); + }); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); + }, + null, + 0 + ); + } +); + +add_task(async function test_shutdown() { + gBrowser.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js b/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js new file mode 100644 index 0000000000..b36d9ecea3 --- /dev/null +++ b/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); + +// This obj will be used in both tests +// First test makes sure accepting the preferences matches these values +// Second test makes sure the cancel dialog STILL matches these values +const syncPrefs = { + "services.sync.engine.addons": false, + "services.sync.engine.bookmarks": true, + "services.sync.engine.history": true, + "services.sync.engine.tabs": false, + "services.sync.engine.prefs": false, + "services.sync.engine.passwords": false, + "services.sync.engine.addresses": false, + "services.sync.engine.creditcards": false, +}; + +add_setup(async () => { + UIState._internal.notifyStateUpdated = () => {}; + const origNotifyStateUpdated = UIState._internal.notifyStateUpdated; + const origGet = UIState.get; + UIState.get = () => { + return { status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" }; + }; + + registerCleanupFunction(() => { + UIState._internal.notifyStateUpdated = origNotifyStateUpdated; + UIState.get = origGet; + }); +}); + +/** + * We don't actually enable sync here, but we just check that the preferences are correct + * when the callback gets hit (accepting/cancelling the dialog) + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1584132. + */ + +add_task(async function testDialogAccept() { + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // This will check if the callback was actually called during the test + let callbackCalled = false; + + // Enabling all the sync UI is painful in tests, so we just open the dialog manually + let syncWindow = await openAndLoadSubDialog( + "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml", + null, + {}, + () => { + for (const [prefKey, prefValue] of Object.entries(syncPrefs)) { + Assert.equal( + Services.prefs.getBoolPref(prefKey), + prefValue, + `${prefValue} is expected value` + ); + } + callbackCalled = true; + } + ); + + Assert.ok(syncWindow, "Choose what to sync window opened"); + let syncChooseDialog = + syncWindow.document.getElementById("syncChooseOptions"); + let syncCheckboxes = syncChooseDialog.querySelectorAll( + "checkbox[preference]" + ); + + // Adjust the checkbox values to the expectedValues in the list + [...syncCheckboxes].forEach(checkbox => { + if (syncPrefs[checkbox.getAttribute("preference")] !== checkbox.checked) { + checkbox.click(); + } + }); + + syncChooseDialog.acceptDialog(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.ok(callbackCalled, "Accept callback was called"); +}); + +add_task(async function testDialogCancel() { + const cancelSyncPrefs = { + "services.sync.engine.addons": true, + "services.sync.engine.bookmarks": false, + "services.sync.engine.history": true, + "services.sync.engine.tabs": true, + "services.sync.engine.prefs": false, + "services.sync.engine.passwords": true, + "services.sync.engine.addresses": true, + "services.sync.engine.creditcards": false, + }; + + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // This will check if the callback was actually called during the test + let callbackCalled = false; + + // Enabling all the sync UI is painful in tests, so we just open the dialog manually + let syncWindow = await openAndLoadSubDialog( + "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml", + null, + {}, + () => { + // We want to test against our previously accepted values in the last test + for (const [prefKey, prefValue] of Object.entries(syncPrefs)) { + Assert.equal( + Services.prefs.getBoolPref(prefKey), + prefValue, + `${prefValue} is expected value` + ); + } + callbackCalled = true; + } + ); + + ok(syncWindow, "Choose what to sync window opened"); + let syncChooseDialog = + syncWindow.document.getElementById("syncChooseOptions"); + let syncCheckboxes = syncChooseDialog.querySelectorAll( + "checkbox[preference]" + ); + + // This time we're adjusting to the cancel list + [...syncCheckboxes].forEach(checkbox => { + if ( + cancelSyncPrefs[checkbox.getAttribute("preference")] !== checkbox.checked + ) { + checkbox.click(); + } + }); + + syncChooseDialog.cancelDialog(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.ok(callbackCalled, "Cancel callback was called"); +}); + +/** + * Tests that this subdialog can be opened via + * about:preferences?action=choose-what-to-sync#sync + */ +add_task(async function testDialogLaunchFromURI() { + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", true]], + }); + + let dialogEventPromise = BrowserTestUtils.waitForEvent( + window, + "dialogopen", + true + ); + await BrowserTestUtils.withNewTab( + "about:preferences?action=choose-what-to-sync#sync", + async browser => { + let dialogEvent = await dialogEventPromise; + Assert.equal( + dialogEvent.detail.dialog._frame.contentWindow.location, + "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml" + ); + } + ); +}); diff --git a/browser/components/preferences/tests/browser_sync_disabled.js b/browser/components/preferences/tests/browser_sync_disabled.js new file mode 100644 index 0000000000..1f8518a1e4 --- /dev/null +++ b/browser/components/preferences/tests/browser_sync_disabled.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't show sync pane when it's disabled. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1536752. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", false]], + }); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + ok( + gBrowser.contentDocument.getElementById("category-sync").hidden, + "sync category hidden" + ); + + // Check that we don't get any results in sync when searching: + await evaluateSearchResults("sync", "no-results-message"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_sync_pairing.js b/browser/components/preferences/tests/browser_sync_pairing.js new file mode 100644 index 0000000000..6491007a38 --- /dev/null +++ b/browser/components/preferences/tests/browser_sync_pairing.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); +const { FxAccountsPairingFlow } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsPairing.sys.mjs" +); + +// Use sinon for mocking. +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +let flowCounter = 0; + +add_setup(async function () { + Services.prefs.setBoolPref("identity.fxaccounts.pairing.enabled", true); + // Sync start-up might interfere with our tests, don't let UIState send UI updates. + const origNotifyStateUpdated = UIState._internal.notifyStateUpdated; + UIState._internal.notifyStateUpdated = () => {}; + + const origGet = UIState.get; + UIState.get = () => { + return { status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" }; + }; + + const origStart = FxAccountsPairingFlow.start; + FxAccountsPairingFlow.start = ({ emitter: e }) => { + return `https://foo.bar/${flowCounter++}`; + }; + + registerCleanupFunction(() => { + UIState._internal.notifyStateUpdated = origNotifyStateUpdated; + UIState.get = origGet; + FxAccountsPairingFlow.start = origStart; + }); +}); + +add_task(async function testShowsQRCode() { + await runWithPairingDialog(async win => { + let doc = win.document; + let qrContainer = doc.getElementById("qrContainer"); + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "ready" + ); + + // Verify that a QRcode is being shown. + Assert.ok( + qrContainer.style.backgroundImage.startsWith( + `url("data:image/gif;base64,R0lGODdhOgA6AIAAAAAAAP///ywAAAAAOgA6AAAC/4yPqcvtD6OctNqLs968+w+G4gKU5nkiJYO2JuW6KsDGKEw3a7AbPZ+r4Ry7nzFIQkKKN6Avlzowo78` + ) + ); + + // Close the dialog. + let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + gBrowser.contentDocument.querySelector(".dialogClose").click(); + + info("waiting for dialog to unload"); + await promiseUnloaded; + }); +}); + +add_task(async function testCantShowQrCode() { + const origStart = FxAccountsPairingFlow.start; + FxAccountsPairingFlow.start = async () => { + throw new Error("boom"); + }; + await runWithPairingDialog(async win => { + let doc = win.document; + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "error" + ); + + // Close the dialog. + let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + gBrowser.contentDocument.querySelector(".dialogClose").click(); + + info("waiting for dialog to unload"); + await promiseUnloaded; + }); + FxAccountsPairingFlow.start = origStart; +}); + +add_task(async function testSwitchToWebContent() { + await runWithPairingDialog(async win => { + let doc = win.document; + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "ready" + ); + + const spySwitchURL = sinon.spy(win.gFxaPairDeviceDialog, "_switchToUrl"); + const emitter = win.gFxaPairDeviceDialog._emitter; + emitter.emit("view:SwitchToWebContent", "about:robots"); + + Assert.equal(spySwitchURL.callCount, 1); + }); +}); + +add_task(async function testError() { + await runWithPairingDialog(async win => { + let doc = win.document; + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "ready" + ); + + const emitter = win.gFxaPairDeviceDialog._emitter; + emitter.emit("view:Error"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "error" + ); + + // Close the dialog. + let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + gBrowser.contentDocument.querySelector(".dialogClose").click(); + + info("waiting for dialog to unload"); + await promiseUnloaded; + }); +}); + +async function runWithPairingDialog(test) { + await openPreferencesViaOpenPreferencesAPI("paneSync", { leaveOpen: true }); + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/fxaPairDevice.xhtml" + ); + gBrowser.contentWindow.gSyncPane.pairAnotherDevice(); + + let win = await promiseSubDialogLoaded; + + await test(win); + + sinon.restore(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} diff --git a/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js b/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js new file mode 100644 index 0000000000..8d1fa3c80b --- /dev/null +++ b/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function checkForPrompt(prefVal) { + return async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.history.custom", true], + ["browser.privatebrowsing.autostart", !prefVal], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + is( + doc.getElementById("historyMode").value, + "custom", + "Expect custom history mode" + ); + + // Stub out the prompt method as an easy way to check it was shown. We throw away + // the tab straight after so don't need to bother restoring it. + let promptFired = false; + doc.defaultView.confirmRestartPrompt = () => { + promptFired = true; + return doc.defaultView.CONFIRM_RESTART_PROMPT_RESTART_NOW; + }; + // Tick the checkbox and pretend the user did it: + let checkbox = doc.getElementById("privateBrowsingAutoStart"); + checkbox.checked = prefVal; + checkbox.doCommand(); + + // Now the prompt should have shown. + ok( + promptFired, + `Expect a prompt when turning permanent private browsing ${ + prefVal ? "on" : "off" + }!` + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }; +} + +/** + * Check we show the prompt if the permanent private browsing pref is false + * and we flip the checkbox to true. + */ +add_task(checkForPrompt(true)); + +/** + * Check it works in the other direction: + */ +add_task(checkForPrompt(false)); diff --git a/browser/components/preferences/tests/empty_pdf_file.pdf b/browser/components/preferences/tests/empty_pdf_file.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/browser/components/preferences/tests/empty_pdf_file.pdf diff --git a/browser/components/preferences/tests/engine1/manifest.json b/browser/components/preferences/tests/engine1/manifest.json new file mode 100644 index 0000000000..5fa44ea692 --- /dev/null +++ b/browser/components/preferences/tests/engine1/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine1", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine1@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine1", + "search_url": "https://1.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/browser/components/preferences/tests/engine2/manifest.json b/browser/components/preferences/tests/engine2/manifest.json new file mode 100644 index 0000000000..7ab094198b --- /dev/null +++ b/browser/components/preferences/tests/engine2/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine2", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine2@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine2", + "search_url": "https://2.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/browser/components/preferences/tests/head.js b/browser/components/preferences/tests/head.js new file mode 100644 index 0000000000..3eb126e1ae --- /dev/null +++ b/browser/components/preferences/tests/head.js @@ -0,0 +1,334 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const kDefaultWait = 2000; + +function is_element_visible(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(!BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function is_element_hidden(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function open_preferences(aCallback) { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:preferences"); + let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + newTabBrowser.addEventListener( + "Initialized", + function () { + aCallback(gBrowser.contentWindow); + }, + { capture: true, once: true } + ); +} + +function openAndLoadSubDialog( + aURL, + aFeatures = null, + aParams = null, + aClosingCallback = null +) { + let promise = promiseLoadSubDialog(aURL); + content.gSubDialog.open( + aURL, + { features: aFeatures, closingCallback: aClosingCallback }, + aParams + ); + return promise; +} + +function promiseLoadSubDialog(aURL) { + return new Promise((resolve, reject) => { + content.gSubDialog._dialogStack.addEventListener( + "dialogopen", + function dialogopen(aEvent) { + if ( + aEvent.detail.dialog._frame.contentWindow.location == "about:blank" + ) { + return; + } + content.gSubDialog._dialogStack.removeEventListener( + "dialogopen", + dialogopen + ); + + is( + aEvent.detail.dialog._frame.contentWindow.location.toString(), + aURL, + "Check the proper URL is loaded" + ); + + // Check visibility + is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible"); + + // Check that stylesheets were injected + let expectedStyleSheetURLs = + aEvent.detail.dialog._injectedStyleSheets.slice(0); + for (let styleSheet of aEvent.detail.dialog._frame.contentDocument + .styleSheets) { + let i = expectedStyleSheetURLs.indexOf(styleSheet.href); + if (i >= 0) { + info("found " + styleSheet.href); + expectedStyleSheetURLs.splice(i, 1); + } + } + is( + expectedStyleSheetURLs.length, + 0, + "All expectedStyleSheetURLs should have been found" + ); + + // Wait for the next event tick to make sure the remaining part of the + // testcase runs after the dialog gets ready for input. + executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow)); + } + ); + }); +} + +async function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) { + let finalPaneEvent = Services.prefs.getBoolPref("identity.fxaccounts.enabled") + ? "sync-pane-loaded" + : "privacy-pane-loaded"; + let finalPrefPaneLoaded = TestUtils.topicObserved(finalPaneEvent, () => true); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + openPreferences(aPane, aOptions); + let newTabBrowser = gBrowser.selectedBrowser; + + if (!newTabBrowser.contentWindow) { + await BrowserTestUtils.waitForEvent(newTabBrowser, "Initialized", true); + await BrowserTestUtils.waitForEvent(newTabBrowser.contentWindow, "load"); + await finalPrefPaneLoaded; + } + + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + if (!aOptions || !aOptions.leaveOpen) { + gBrowser.removeCurrentTab(); + } + return { selectedPane }; +} + +async function runSearchInput(input) { + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + searchInput.focus(); + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == input + ); + EventUtils.sendString(input); + await searchCompletedPromise; +} + +async function evaluateSearchResults( + keyword, + searchResults, + includeExperiments = false +) { + searchResults = Array.isArray(searchResults) + ? searchResults + : [searchResults]; + searchResults.push("header-searchResults"); + + await runSearchInput(keyword); + + let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane"); + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if (!includeExperiments && child.id?.startsWith("pane-experimental")) { + continue; + } + if (searchResults.includes(child.id)) { + is_element_visible(child, `${child.id} should be in search results`); + } else if (child.id) { + is_element_hidden(child, `${child.id} should not be in search results`); + } + } +} + +function waitForMutation(target, opts, cb) { + return new Promise(resolve => { + let observer = new MutationObserver(() => { + if (!cb || cb(target)) { + observer.disconnect(); + resolve(); + } + }); + observer.observe(target, opts); + }); +} + +// Used to add sample experimental features for testing. To use, create +// a DefinitionServer, then call addDefinition as needed. +class DefinitionServer { + constructor(definitionOverrides = []) { + let { HttpServer } = ChromeUtils.import( + "resource://testing-common/httpd.js" + ); + + this.server = new HttpServer(); + this.server.registerPathHandler("/definitions.json", this); + this.definitions = {}; + + for (const override of definitionOverrides) { + this.addDefinition(override); + } + + this.server.start(); + registerCleanupFunction( + () => new Promise(resolve => this.server.stop(resolve)) + ); + } + + // for nsIHttpRequestHandler + handle(request, response) { + response.write(JSON.stringify(this.definitions)); + } + + get definitionsUrl() { + const { primaryScheme, primaryHost, primaryPort } = this.server.identity; + return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`; + } + + addDefinition(overrides = {}) { + const definition = { + id: "test-feature", + // These l10n IDs are just random so we have some text to display + title: "experimental-features-media-jxl", + description: "pane-experimental-description2", + restartRequired: false, + type: "boolean", + preference: "test.feature", + defaultValue: false, + isPublic: false, + ...overrides, + }; + // convert targeted values, used by fromId + definition.isPublic = { default: definition.isPublic }; + definition.defaultValue = { default: definition.defaultValue }; + this.definitions[definition.id] = definition; + return definition; + } +} + +/** + * Creates observer that waits for and then compares all perm-changes with the observances in order. + * @param {Array} observances permission changes to observe (order is important) + * @returns {Promise} Promise object that resolves once all permission changes have been observed + */ +function createObserveAllPromise(observances) { + // Create new promise that resolves once all items + // in observances array have been observed. + return new Promise(resolve => { + let permObserver = { + observe(aSubject, aTopic, aData) { + if (aTopic != "perm-changed") { + return; + } + + if (!observances.length) { + // See bug 1063410 + return; + } + + let permission = aSubject.QueryInterface(Ci.nsIPermission); + let expected = observances.shift(); + + info( + `observed perm-changed for ${permission.principal.origin} (remaining ${observances.length})` + ); + + is(aData, expected.data, "type of message should be the same"); + for (let prop of ["type", "capability", "expireType"]) { + if (expected[prop]) { + is( + permission[prop], + expected[prop], + `property: "${prop}" should be equal (${permission.principal.origin})` + ); + } + } + + if (expected.origin) { + is( + permission.principal.origin, + expected.origin, + `property: "origin" should be equal (${permission.principal.origin})` + ); + } + + if (!observances.length) { + Services.obs.removeObserver(permObserver, "perm-changed"); + executeSoon(resolve); + } + }, + }; + Services.obs.addObserver(permObserver, "perm-changed"); + }); +} + +/** + * Waits for preference to be set and asserts the value. + * @param {string} pref - Preference key. + * @param {*} expectedValue - Expected value of the preference. + * @param {string} message - Assertion message. + */ +async function waitForAndAssertPrefState(pref, expectedValue, message) { + await TestUtils.waitForPrefChange(pref, value => { + if (value != expectedValue) { + return false; + } + is(value, expectedValue, message); + return true; + }); +} + +/** + * The Relay promo is not shown for distributions with a custom FxA instance, + * since Relay requires an account on our own server. These prefs are set to a + * dummy address by the test harness, filling the prefs with a "user value." + * This temporarily sets the default value equal to the dummy value, so that + * Firefox thinks we've configured the correct FxA server. + * @returns {Promise<MockFxAUtilityFunctions>} { mock, unmock } + */ +async function mockDefaultFxAInstance() { + /** + * @typedef {Object} MockFxAUtilityFunctions + * @property {function():void} mock - Makes the dummy values default, creating + * the illusion of a production FxA instance. + * @property {function():void} unmock - Restores the true defaults, creating + * the illusion of a custom FxA instance. + */ + + const defaultPrefs = Services.prefs.getDefaultBranch(""); + const userPrefs = Services.prefs.getBranch(""); + const realAuth = defaultPrefs.getCharPref("identity.fxaccounts.auth.uri"); + const realRoot = defaultPrefs.getCharPref("identity.fxaccounts.remote.root"); + const mockAuth = userPrefs.getCharPref("identity.fxaccounts.auth.uri"); + const mockRoot = userPrefs.getCharPref("identity.fxaccounts.remote.root"); + const mock = () => { + defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth); + defaultPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot); + userPrefs.clearUserPref("identity.fxaccounts.auth.uri"); + userPrefs.clearUserPref("identity.fxaccounts.remote.root"); + }; + const unmock = () => { + defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", realAuth); + defaultPrefs.setCharPref("identity.fxaccounts.remote.root", realRoot); + userPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth); + userPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot); + }; + + mock(); + registerCleanupFunction(unmock); + + return { mock, unmock }; +} diff --git a/browser/components/preferences/tests/privacypane_tests_perwindow.js b/browser/components/preferences/tests/privacypane_tests_perwindow.js new file mode 100644 index 0000000000..8c376d1705 --- /dev/null +++ b/browser/components/preferences/tests/privacypane_tests_perwindow.js @@ -0,0 +1,388 @@ +// This file gets imported into the same scope as head.js. +/* import-globals-from head.js */ + +async function runTestOnPrivacyPrefPane(testFunc) { + info("runTestOnPrivacyPrefPane entered"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences", + true, + true + ); + let browser = tab.linkedBrowser; + info("loaded about:preferences"); + await browser.contentWindow.gotoPref("panePrivacy"); + info("viewing privacy pane, executing testFunc"); + await testFunc(browser.contentWindow); + BrowserTestUtils.removeTab(tab); +} + +function controlChanged(element) { + element.doCommand(); +} + +// We can only test the panes that don't trigger a preference update +function test_pane_visibility(win) { + let modes = { + remember: "historyRememberPane", + custom: "historyCustomPane", + }; + + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let historypane = win.document.getElementById("historyPane"); + ok(historypane, "history mode pane should exist"); + + for (let mode in modes) { + historymode.value = mode; + controlChanged(historymode); + is( + historypane.selectedPanel, + win.document.getElementById(modes[mode]), + "The correct pane should be selected for the " + mode + " mode" + ); + is_element_visible( + historypane.selectedPanel, + "Correct pane should be visible for the " + mode + " mode" + ); + } +} + +function test_dependent_elements(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let pbautostart = win.document.getElementById("privateBrowsingAutoStart"); + ok(pbautostart, "the private browsing auto-start checkbox should exist"); + let controls = [ + win.document.getElementById("rememberHistory"), + win.document.getElementById("rememberForms"), + win.document.getElementById("deleteOnClose"), + win.document.getElementById("alwaysClear"), + ]; + controls.forEach(function (control) { + ok(control, "the dependent controls should exist"); + }); + let independents = [ + win.document.getElementById("contentBlockingBlockCookiesCheckbox"), + ]; + independents.forEach(function (control) { + ok(control, "the independent controls should exist"); + }); + let cookieexceptions = win.document.getElementById("cookieExceptions"); + ok(cookieexceptions, "the cookie exceptions button should exist"); + let deleteOnCloseCheckbox = win.document.getElementById("deleteOnClose"); + ok(deleteOnCloseCheckbox, "the delete on close checkbox should exist"); + let alwaysclear = win.document.getElementById("alwaysClear"); + ok(alwaysclear, "the clear data on close checkbox should exist"); + let rememberhistory = win.document.getElementById("rememberHistory"); + ok(rememberhistory, "the remember history checkbox should exist"); + let rememberforms = win.document.getElementById("rememberForms"); + ok(rememberforms, "the remember forms checkbox should exist"); + let alwaysclearsettings = win.document.getElementById("clearDataSettings"); + ok(alwaysclearsettings, "the clear data settings button should exist"); + + function expect_disabled(disabled) { + controls.forEach(function (control) { + is( + control.disabled, + disabled, + control.getAttribute("id") + + " should " + + (disabled ? "" : "not ") + + "be disabled" + ); + }); + if (disabled) { + ok( + !alwaysclear.checked, + "the clear data on close checkbox value should be as expected" + ); + ok( + !rememberhistory.checked, + "the remember history checkbox value should be as expected" + ); + ok( + !rememberforms.checked, + "the remember forms checkbox value should be as expected" + ); + } + } + function check_independents(expected) { + independents.forEach(function (control) { + is( + control.disabled, + expected, + control.getAttribute("id") + + " should " + + (expected ? "" : "not ") + + "be disabled" + ); + }); + + ok( + !cookieexceptions.disabled, + "the cookie exceptions button should never be disabled" + ); + ok( + alwaysclearsettings.disabled, + "the clear data settings button should always be disabled" + ); + } + + // controls should only change in custom mode + historymode.value = "remember"; + controlChanged(historymode); + expect_disabled(false); + check_independents(false); + + // setting the mode to custom shouldn't change anything + historymode.value = "custom"; + controlChanged(historymode); + expect_disabled(false); + check_independents(false); +} + +function test_dependent_cookie_elements(win) { + let deleteOnCloseCheckbox = win.document.getElementById("deleteOnClose"); + let deleteOnCloseNote = win.document.getElementById("deleteOnCloseNote"); + let blockCookiesMenu = win.document.getElementById("blockCookiesMenu"); + + let controls = [blockCookiesMenu, deleteOnCloseCheckbox]; + controls.forEach(function (control) { + ok(control, "the dependent cookie controls should exist"); + }); + let blockCookiesCheckbox = win.document.getElementById( + "contentBlockingBlockCookiesCheckbox" + ); + ok(blockCookiesCheckbox, "the block cookies checkbox should exist"); + + function expect_disabled(disabled, c = controls) { + c.forEach(function (control) { + is( + control.disabled, + disabled, + control.getAttribute("id") + + " should " + + (disabled ? "" : "not ") + + "be disabled" + ); + }); + } + + blockCookiesCheckbox.checked = true; + controlChanged(blockCookiesCheckbox); + expect_disabled(false); + + blockCookiesCheckbox.checked = false; + controlChanged(blockCookiesCheckbox); + expect_disabled(true, [blockCookiesMenu]); + expect_disabled(false, [deleteOnCloseCheckbox]); + is_element_hidden( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be hidden." + ); + + blockCookiesMenu.value = "always"; + controlChanged(blockCookiesMenu); + expect_disabled(true, [deleteOnCloseCheckbox]); + expect_disabled(false, [blockCookiesMenu]); + is_element_hidden( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be hidden." + ); + + if (win.contentBlockingCookiesAndSiteDataRejectTrackersEnabled) { + blockCookiesMenu.value = "trackers"; + } else { + blockCookiesMenu.value = "unvisited"; + } + controlChanged(blockCookiesMenu); + expect_disabled(false); + + let historymode = win.document.getElementById("historyMode"); + + // The History mode setting for "never remember history" should still + // disable the "keep cookies until..." menu. + historymode.value = "dontremember"; + controlChanged(historymode); + expect_disabled(true, [deleteOnCloseCheckbox]); + is_element_visible( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be visible." + ); + expect_disabled(false, [blockCookiesMenu]); + + historymode.value = "remember"; + controlChanged(historymode); + expect_disabled(false); + is_element_hidden( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be hidden." + ); +} + +function test_dependent_clearonclose_elements(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let pbautostart = win.document.getElementById("privateBrowsingAutoStart"); + ok(pbautostart, "the private browsing auto-start checkbox should exist"); + let alwaysclear = win.document.getElementById("alwaysClear"); + ok(alwaysclear, "the clear data on close checkbox should exist"); + let alwaysclearsettings = win.document.getElementById("clearDataSettings"); + ok(alwaysclearsettings, "the clear data settings button should exist"); + + function expect_disabled(disabled) { + is( + alwaysclearsettings.disabled, + disabled, + "the clear data settings should " + + (disabled ? "" : "not ") + + "be disabled" + ); + } + + historymode.value = "custom"; + controlChanged(historymode); + pbautostart.checked = false; + controlChanged(pbautostart); + alwaysclear.checked = false; + controlChanged(alwaysclear); + expect_disabled(true); + + alwaysclear.checked = true; + controlChanged(alwaysclear); + expect_disabled(false); + + alwaysclear.checked = false; + controlChanged(alwaysclear); + expect_disabled(true); +} + +async function test_dependent_prefs(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let controls = [ + win.document.getElementById("rememberHistory"), + win.document.getElementById("rememberForms"), + ]; + controls.forEach(function (control) { + ok(control, "the micro-management controls should exist"); + }); + + function expect_checked(checked) { + controls.forEach(function (control) { + is( + control.checked, + checked, + control.getAttribute("id") + + " should " + + (checked ? "" : "not ") + + "be checked" + ); + }); + } + + // controls should be checked in remember mode + historymode.value = "remember"; + controlChanged(historymode); + // Initial updates from prefs are not sync, so wait: + await TestUtils.waitForCondition( + () => controls[0].getAttribute("checked") == "true" + ); + expect_checked(true); + + // even if they're unchecked in custom mode + historymode.value = "custom"; + controlChanged(historymode); + controls.forEach(function (control) { + control.checked = false; + controlChanged(control); + }); + expect_checked(false); + historymode.value = "remember"; + controlChanged(historymode); + expect_checked(true); +} + +function test_historymode_retention(mode, expect) { + return function test_historymode_retention_fn(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + + if ( + (historymode.value == "remember" && mode == "dontremember") || + (historymode.value == "dontremember" && mode == "remember") || + (historymode.value == "custom" && mode == "dontremember") + ) { + return; + } + + if (expect !== undefined) { + is( + historymode.value, + expect, + "history mode is expected to remain " + expect + ); + } + + historymode.value = mode; + controlChanged(historymode); + }; +} + +function test_custom_retention(controlToChange, expect, valueIncrement) { + return function test_custom_retention_fn(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + + if (expect !== undefined) { + is( + historymode.value, + expect, + "history mode is expected to remain " + expect + ); + } + + historymode.value = "custom"; + controlChanged(historymode); + + controlToChange = win.document.getElementById(controlToChange); + ok(controlToChange, "the control to change should exist"); + switch (controlToChange.localName) { + case "checkbox": + controlToChange.checked = !controlToChange.checked; + break; + case "menulist": + controlToChange.value = valueIncrement; + break; + } + controlChanged(controlToChange); + }; +} + +const gPrefCache = new Map(); + +function cache_preferences(win) { + let prefs = win.Preferences.getAll(); + for (let pref of prefs) { + gPrefCache.set(pref.id, pref.value); + } +} + +function reset_preferences(win) { + let prefs = win.Preferences.getAll(); + // Avoid assigning undefined, which means clearing a "user"/test pref value + for (let pref of prefs) { + if (gPrefCache.has(pref.id)) { + pref.value = gPrefCache.get(pref.id); + } + } +} + +function run_test_subset(subset) { + info("subset: " + Array.from(subset, x => x.name).join(",") + "\n"); + let tests = [cache_preferences, ...subset, reset_preferences]; + for (let test of tests) { + add_task(runTestOnPrivacyPrefPane.bind(undefined, test)); + } +} diff --git a/browser/components/preferences/tests/siteData/browser.ini b/browser/components/preferences/tests/siteData/browser.ini new file mode 100644 index 0000000000..deb8e82f7d --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser.ini @@ -0,0 +1,22 @@ +[DEFAULT] +support-files = + head.js + site_data_test.html + service_worker_test.html + service_worker_test.js + offline/offline.html + offline/manifest.appcache + +[browser_clearSiteData.js] +[browser_siteData.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_siteData2.js] +skip-if = + win10_2004 && (!debug && !asan) # Bug 1669937 + win11_2009 && (!debug && !asan) # Bug 1797751 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + apple_catalina && debug # Bug 1775910 +[browser_siteData3.js] +[browser_siteData_multi_select.js] +skip-if = tsan # Bug 1683730 diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/tests/siteData/browser_clearSiteData.js new file mode 100644 index 0000000000..217656707e --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_clearSiteData.js @@ -0,0 +1,219 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +async function testClearData(clearSiteData, clearCache) { + PermissionTestUtils.add( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage", + Services.perms.ALLOW_ACTION + ); + + // Open a test site which saves into appcache. + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Fill indexedDB with test data. + // Don't wait for the page to load, to register the content event handler as quickly as possible. + // If this test goes intermittent, we might have to tell the page to wait longer before + // firing the event. + BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false); + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "test-indexedDB-done", + false, + null, + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Register some service workers. + await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL); + await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // Test the initial states. + let cacheUsage = await SiteDataManager.getCacheSize(); + let quotaUsage = await SiteDataTestUtils.getQuotaUsage( + TEST_QUOTA_USAGE_ORIGIN + ); + let totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(cacheUsage, 0, "The cache usage should not be 0"); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + + let initialSizeLabelValue = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + let sizeLabel = content.document.getElementById("totalSiteDataSize"); + return sizeLabel.textContent; + } + ); + + let doc = gBrowser.selectedBrowser.contentDocument; + let clearSiteDataButton = doc.getElementById("clearSiteDataButton"); + + let dialogOpened = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml" + ); + clearSiteDataButton.doCommand(); + let dialogWin = await dialogOpened; + + // Convert the usage numbers in the same way the UI does it to assert + // that they're displayed in the dialog. + let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage); + // For cache we just assert that the right unit (KB, probably) is displayed, + // since we've had cache intermittently changing under our feet. + let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage); + + let clearSiteDataCheckbox = + dialogWin.document.getElementById("clearSiteData"); + let clearCacheCheckbox = dialogWin.document.getElementById("clearCache"); + // The usage details are filled asynchronously, so we assert that they're present by + // waiting for them to be filled in. + await Promise.all([ + TestUtils.waitForCondition( + () => + clearSiteDataCheckbox.label && + clearSiteDataCheckbox.label.includes(convertedTotalUsage), + "Should show the quota usage" + ), + TestUtils.waitForCondition( + () => + clearCacheCheckbox.label && + clearCacheCheckbox.label.includes(convertedCacheUnit), + "Should show the cache usage" + ), + ]); + + // Check the boxes according to our test input. + clearSiteDataCheckbox.checked = clearSiteData; + clearCacheCheckbox.checked = clearCache; + + // Some additional promises/assertions to wait for + // when deleting site data. + let acceptPromise; + let updatePromise; + let cookiesClearedPromise; + if (clearSiteData) { + acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + updatePromise = promiseSiteDataManagerSitesUpdated(); + cookiesClearedPromise = promiseCookiesCleared(); + } + + let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + + let clearButton = dialogWin.document + .querySelector("dialog") + .getButton("accept"); + if (!clearSiteData && !clearCache) { + // Simulate user input on one of the checkboxes to trigger the event listener for + // disabling the clearButton. + clearCacheCheckbox.doCommand(); + // Check that the clearButton gets disabled by unchecking both options. + await TestUtils.waitForCondition( + () => clearButton.disabled, + "Clear button should be disabled" + ); + let cancelButton = dialogWin.document + .querySelector("dialog") + .getButton("cancel"); + // Cancel, since we can't delete anything. + cancelButton.click(); + } else { + // Delete stuff! + clearButton.click(); + } + + // For site data we display an extra warning dialog, make sure + // to accept it. + if (clearSiteData) { + await acceptPromise; + } + + await dialogClosed; + + if (clearCache) { + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getCacheSize(); + return usage == 0; + }, "The cache usage should be removed"); + } else { + Assert.greater( + await SiteDataManager.getCacheSize(), + 0, + "The cache usage should not be 0" + ); + } + + if (clearSiteData) { + await updatePromise; + await cookiesClearedPromise; + await promiseServiceWorkersCleared(); + + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getTotalUsage(); + return usage == 0; + }, "The total usage should be removed"); + } else { + quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN); + totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + } + + if (clearCache || clearSiteData) { + // Check that the size label in about:preferences updates after we cleared data. + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ initialSizeLabelValue }], + async function (opts) { + let sizeLabel = content.document.getElementById("totalSiteDataSize"); + await ContentTaskUtils.waitForCondition( + () => sizeLabel.textContent != opts.initialSizeLabelValue, + "Site data size label should have updated." + ); + } + ); + } + + let permission = PermissionTestUtils.getPermissionObject( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage" + ); + is( + clearSiteData ? permission : permission.capability, + clearSiteData ? null : Services.perms.ALLOW_ACTION, + "Should have the correct permission state." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SiteDataManager.removeAll(); +} + +// Test opening the "Clear All Data" dialog and cancelling. +add_task(async function () { + await testClearData(false, false); +}); + +// Test opening the "Clear All Data" dialog and removing all site data. +add_task(async function () { + await testClearData(true, false); +}); + +// Test opening the "Clear All Data" dialog and removing all cache. +add_task(async function () { + await testClearData(false, true); +}); + +// Test opening the "Clear All Data" dialog and removing everything. +add_task(async function () { + await testClearData(true, true); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData.js b/browser/components/preferences/tests/siteData/browser_siteData.js new file mode 100644 index 0000000000..d3a73d6f4b --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData.js @@ -0,0 +1,400 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function getPersistentStoragePermStatus(origin) { + let uri = Services.io.newURI(origin); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + return Services.perms.testExactPermissionFromPrincipal( + principal, + "persistent-storage" + ); +} + +// Test listing site using quota usage or site using appcache +// This is currently disabled because of bug 1414751. +add_task(async function () { + // Open a test site which would save into appcache + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Open a test site which would save into quota manager + BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL); + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "test-indexedDB-done", + false, + null, + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + let updatedPromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatedPromise; + await openSiteDataSettingsDialog(); + let dialog = content.gSubDialog._topDialog; + let dialogFrame = dialog._frame; + let frameDoc = dialogFrame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 2, "Should list sites using quota usage or appcache"); + + let appcacheSite = frameDoc.querySelector( + `richlistitem[host="${TEST_OFFLINE_HOST}"]` + ); + ok(appcacheSite, "Should list site using appcache"); + + let qoutaUsageSite = frameDoc.querySelector( + `richlistitem[host="${TEST_QUOTA_USAGE_HOST}"]` + ); + ok(qoutaUsageSite, "Should list site using quota usage"); + + // Always remember to clean up + await new Promise(resolve => { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TEST_QUOTA_USAGE_ORIGIN + ); + let request = Services.qms.clearStoragesForPrincipal( + principal, + null, + null, + true + ); + request.callback = resolve; + }); + + await SiteDataManager.removeAll(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}).skip(); // Bug 1414751 + +// Test buttons are disabled and loading message shown while updating sites +add_task(async function () { + let updatedPromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatedPromise; + let cacheSize = await SiteDataManager.getCacheSize(); + + let doc = gBrowser.selectedBrowser.contentDocument; + let clearBtn = doc.getElementById("clearSiteDataButton"); + let settingsButton = doc.getElementById("siteDataSettings"); + let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize"); + is( + clearBtn.disabled, + false, + "Should enable clear button after sites updated" + ); + is( + settingsButton.disabled, + false, + "Should enable settings button after sites updated" + ); + await SiteDataManager.getTotalUsage().then(usage => { + let [value, unit] = DownloadUtils.convertByteUnits(usage + cacheSize); + Assert.deepEqual( + doc.l10n.getAttributes(totalSiteDataSizeLabel), + { + id: "sitedata-total-size", + args: { value, unit }, + }, + "Should show the right total site data size" + ); + }); + + Services.obs.notifyObservers(null, "sitedatamanager:updating-sites"); + is( + clearBtn.disabled, + true, + "Should disable clear button while updating sites" + ); + is( + settingsButton.disabled, + true, + "Should disable settings button while updating sites" + ); + Assert.deepEqual( + doc.l10n.getAttributes(totalSiteDataSizeLabel), + { + id: "sitedata-total-size-calculating", + args: null, + }, + "Should show the loading message while updating" + ); + + Services.obs.notifyObservers(null, "sitedatamanager:sites-updated"); + is( + clearBtn.disabled, + false, + "Should enable clear button after sites updated" + ); + is( + settingsButton.disabled, + false, + "Should enable settings button after sites updated" + ); + cacheSize = await SiteDataManager.getCacheSize(); + await SiteDataManager.getTotalUsage().then(usage => { + let [value, unit] = DownloadUtils.convertByteUnits(usage + cacheSize); + Assert.deepEqual( + doc.l10n.getAttributes(totalSiteDataSizeLabel), + { + id: "sitedata-total-size", + args: { value, unit }, + }, + "Should show the right total site data size" + ); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test clearing service worker through the settings panel +add_task(async function () { + // Register a test service worker + await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + // Test the initial states + await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL); + // Open the Site Data Settings panel and remove the site + await openSiteDataSettingsDialog(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + let updatePromise = promiseSiteDataManagerSitesUpdated(); + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ TEST_OFFLINE_HOST }], + args => { + let host = args.TEST_OFFLINE_HOST; + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + let sitesList = frameDoc.getElementById("sitesList"); + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + if (site) { + let removeBtn = frameDoc.getElementById("removeSelected"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + site.click(); + removeBtn.doCommand(); + saveBtn.doCommand(); + } else { + ok(false, `Should have one site of ${host}`); + } + } + ); + await acceptRemovePromise; + await updatePromise; + await promiseServiceWorkersCleared(); + await SiteDataManager.removeAll(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test showing and removing sites with cookies. +add_task(async function () { + // Add some test cookies. + let uri = Services.io.newURI("https://example.com"); + let uri2 = Services.io.newURI("https://example.org"); + Services.cookies.add( + uri.host, + uri.pathQueryRef, + "test1", + "1", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Services.cookies.add( + uri.host, + uri.pathQueryRef, + "test2", + "2", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Services.cookies.add( + uri2.host, + uri2.pathQueryRef, + "test1", + "1", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Ensure that private browsing cookies are ignored. + Services.cookies.add( + uri.host, + uri.pathQueryRef, + "test3", + "3", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + { privateBrowsingId: 1 }, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Get the exact creation date from the cookies (to avoid intermittents + // from minimal time differences, since we round up to minutes). + let cookies1 = Services.cookies.getCookiesFromHost(uri.host, {}); + let cookies2 = Services.cookies.getCookiesFromHost(uri2.host, {}); + // We made two valid cookies for example.com. + let cookie1 = cookies1[1]; + let cookie2 = cookies2[0]; + + let fullFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "short", + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // Open the site data manager and remove one site. + await openSiteDataSettingsDialog(); + let creationDate1 = new Date(cookie1.lastAccessed / 1000); + let creationDate1Formatted = fullFormatter.format(creationDate1); + let creationDate2 = new Date(cookie2.lastAccessed / 1000); + let creationDate2Formatted = fullFormatter.format(creationDate2); + let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [ + { + creationDate1Formatted, + creationDate2Formatted, + }, + ], + function (args) { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 2, "Should list two sites with cookies"); + let sitesList = frameDoc.getElementById("sitesList"); + let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`); + let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`); + + let columns = site1.querySelectorAll(".item-box > label"); + let boxes = site1.querySelectorAll(".item-box"); + is(columns[0].value, "example.com", "Should show the correct host."); + is(columns[1].value, "2", "Should show the correct number of cookies."); + is(columns[2].value, "", "Should show no site data."); + is( + /(now|second)/.test(columns[3].value), + true, + "Should show the relative date." + ); + is( + boxes[3].getAttribute("tooltiptext"), + args.creationDate1Formatted, + "Should show the correct date." + ); + + columns = site2.querySelectorAll(".item-box > label"); + boxes = site2.querySelectorAll(".item-box"); + is(columns[0].value, "example.org", "Should show the correct host."); + is(columns[1].value, "1", "Should show the correct number of cookies."); + is(columns[2].value, "", "Should show no site data."); + is( + /(now|second)/.test(columns[3].value), + true, + "Should show the relative date." + ); + is( + boxes[3].getAttribute("tooltiptext"), + args.creationDate2Formatted, + "Should show the correct date." + ); + + let removeBtn = frameDoc.getElementById("removeSelected"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + site2.click(); + removeBtn.doCommand(); + saveBtn.doCommand(); + } + ); + await removeDialogOpenPromise; + + await TestUtils.waitForCondition( + () => Services.cookies.countCookiesFromHost(uri2.host) == 0, + "Cookies from the first host should be cleared" + ); + is( + Services.cookies.countCookiesFromHost(uri.host), + 2, + "Cookies from the second host should not be cleared" + ); + + // Open the site data manager and remove another site. + await openSiteDataSettingsDialog(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ creationDate1Formatted }], + function (args) { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 1, "Should list one site with cookies"); + let sitesList = frameDoc.getElementById("sitesList"); + let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`); + + let columns = site1.querySelectorAll(".item-box > label"); + let boxes = site1.querySelectorAll(".item-box"); + is(columns[0].value, "example.com", "Should show the correct host."); + is(columns[1].value, "2", "Should show the correct number of cookies."); + is(columns[2].value, "", "Should show no site data."); + is( + /(now|second)/.test(columns[3].value), + true, + "Should show the relative date." + ); + is( + boxes[3].getAttribute("tooltiptext"), + args.creationDate1Formatted, + "Should show the correct date." + ); + + let removeBtn = frameDoc.getElementById("removeSelected"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + site1.click(); + removeBtn.doCommand(); + saveBtn.doCommand(); + } + ); + await acceptRemovePromise; + + await TestUtils.waitForCondition( + () => Services.cookies.countCookiesFromHost(uri.host) == 0, + "Cookies from the second host should be cleared" + ); + + await openSiteDataSettingsDialog(); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 0, "Should list no sites with cookies"); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData2.js b/browser/components/preferences/tests/siteData/browser_siteData2.js new file mode 100644 index 0000000000..863a8dcefe --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData2.js @@ -0,0 +1,475 @@ +"use strict"; + +function assertAllSitesNotListed(win) { + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let removeAllBtn = frameDoc.getElementById("removeAll"); + let sitesList = frameDoc.getElementById("sitesList"); + let sites = sitesList.getElementsByTagName("richlistitem"); + is(sites.length, 0, "Should not list all sites"); + is(removeBtn.disabled, true, "Should disable the removeSelected button"); + is(removeAllBtn.disabled, true, "Should disable the removeAllBtn button"); +} + +// Test selecting and removing all sites one by one +add_task(async function test_selectRemove() { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = null; + let saveBtn = null; + let cancelBtn = null; + let settingsDialogClosePromise = null; + + // Test the initial state + assertSitesListed(doc, hosts); + + // Test the "Cancel" button + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(win); + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button but cancelling save + let cancelPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel"); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(win); + saveBtn.doCommand(); + await cancelPromise; + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button and accepting save + let acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + settingsDialogClosePromise = promiseSettingsDialogClose(); + updatePromise = promiseSiteDataManagerSitesUpdated(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(win); + saveBtn.doCommand(); + await acceptPromise; + await settingsDialogClosePromise; + await updatePromise; + await openSiteDataSettingsDialog(); + assertAllSitesNotListed(win); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + function removeAllSitesOneByOne() { + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let sitesList = frameDoc.getElementById("sitesList"); + let sites = sitesList.getElementsByTagName("richlistitem"); + for (let i = sites.length - 1; i >= 0; --i) { + sites[i].click(); + removeBtn.doCommand(); + } + } +}); + +// Test selecting and removing partial sites +add_task(async function test_removePartialSites() { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + { + usage: 1024, + origin: "https://s3-us-west-2.amazonaws.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://127.0.0.1", + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = null; + let saveBtn = null; + let cancelBtn = null; + let removeDialogOpenPromise = null; + let settingsDialogClosePromise = null; + + // Test the initial state + assertSitesListed(doc, hosts); + + // Test the "Cancel" button + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + await removeSelectedSite(hosts.slice(0, 2)); + assertSitesListed(doc, hosts.slice(2)); + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button but canceling save + removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "cancel", + REMOVE_DIALOG_URL + ); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + await removeSelectedSite(hosts.slice(0, 2)); + assertSitesListed(doc, hosts.slice(2)); + saveBtn.doCommand(); + await removeDialogOpenPromise; + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button and accepting save + removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + await removeSelectedSite(hosts.slice(0, 2)); + assertSitesListed(doc, hosts.slice(2)); + saveBtn.doCommand(); + await removeDialogOpenPromise; + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts.slice(2)); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + function removeSelectedSite(removeHosts) { + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + is( + removeBtn.disabled, + true, + "Should start with disabled removeSelected button" + ); + let sitesList = frameDoc.getElementById("sitesList"); + removeHosts.forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + if (site) { + site.click(); + let currentSelectedIndex = sitesList.selectedIndex; + is( + removeBtn.disabled, + false, + "Should enable the removeSelected button" + ); + removeBtn.doCommand(); + let newSelectedIndex = sitesList.selectedIndex; + if (currentSelectedIndex >= sitesList.itemCount) { + is(newSelectedIndex, currentSelectedIndex - 1); + } else { + is(newSelectedIndex, currentSelectedIndex); + } + } else { + ok(false, `Should not select and remove inexistent site of ${host}`); + } + }); + } +}); + +// Test searching and then removing only visible sites +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + // Search "foo" to only list foo.com sites + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let searchBox = frameDoc.getElementById("searchBox"); + searchBox.value = "xyz"; + searchBox.doCommand(); + assertSitesListed( + doc, + hosts.filter(host => host.includes("xyz")) + ); + + // Test only removing all visible sites listed + updatePromise = promiseSiteDataManagerSitesUpdated(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + let settingsDialogClosePromise = promiseSettingsDialogClose(); + let removeAllBtn = frameDoc.getElementById("removeAll"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + removeAllBtn.doCommand(); + saveBtn.doCommand(); + await acceptRemovePromise; + await settingsDialogClosePromise; + await updatePromise; + await openSiteDataSettingsDialog(); + assertSitesListed( + doc, + hosts.filter(host => !host.includes("xyz")) + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test dynamically clearing all site data +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + // Test the initial state + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + let doc = gBrowser.selectedBrowser.contentDocument; + assertSitesListed(doc, hosts); + + await addTestData([ + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + // Test clearing all site data dynamically + let win = gBrowser.selectedBrowser.contentWindow; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + updatePromise = promiseSiteDataManagerSitesUpdated(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + let settingsDialogClosePromise = promiseSettingsDialogClose(); + let removeAllBtn = frameDoc.getElementById("removeAll"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + removeAllBtn.doCommand(); + saveBtn.doCommand(); + await acceptRemovePromise; + await settingsDialogClosePromise; + await updatePromise; + await openSiteDataSettingsDialog(); + assertAllSitesNotListed(win); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Tests clearing search box content via backspace does not delete site data +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let searchBox = frameDoc.getElementById("searchBox"); + searchBox.value = "xyz"; + searchBox.doCommand(); + assertSitesListed( + doc, + hosts.filter(host => host.includes("xyz")) + ); + + // Make sure the focus is on the search box + searchBox.focus(); + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + } else { + EventUtils.synthesizeKey("VK_DELETE", {}, win); + } + assertSitesListed( + doc, + hosts.filter(host => host.includes("xyz")) + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Tests remove site data via backspace +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + // Test initial state + assertSitesListed(doc, hosts); + + let sitesList = frameDoc.getElementById("sitesList"); + let site = sitesList.querySelector(`richlistitem[host="xyz.com"]`); + if (site) { + // Move the focus from the search box to the list and select an item + sitesList.focus(); + site.click(); + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + } else { + EventUtils.synthesizeKey("VK_DELETE", {}, win); + } + } + + assertSitesListed( + doc, + hosts.filter(host => !host.includes("xyz")) + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData3.js b/browser/components/preferences/tests/siteData/browser_siteData3.js new file mode 100644 index 0000000000..58aa5bf1b9 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData3.js @@ -0,0 +1,327 @@ +"use strict"; + +// Test not displaying sites which store 0 byte and don't have persistent storage. +add_task(async function test_exclusions() { + let hosts = await addTestData([ + { + usage: 0, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 0, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + { + usage: 0, + origin: "http://cookies.bar.com", + cookies: 5, + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + let doc = gBrowser.selectedBrowser.contentDocument; + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + assertSitesListed( + doc, + hosts.filter(host => host != "shopping.xyz.com") + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test grouping and listing sites across scheme, port and origin attributes by base domain. +add_task(async function test_grouping() { + let quotaUsage = 7000000; + let testData = [ + { + usage: quotaUsage, + origin: "https://account.xyz.com^userContextId=1", + cookies: 2, + persisted: true, + }, + { + usage: quotaUsage, + origin: "https://account.xyz.com", + cookies: 1, + persisted: false, + }, + { + usage: quotaUsage, + origin: "https://account.xyz.com:123", + cookies: 1, + persisted: false, + }, + { + usage: quotaUsage, + origin: "http://account.xyz.com", + cookies: 1, + persisted: false, + }, + { + usage: quotaUsage, + origin: "http://search.xyz.com", + cookies: 3, + persisted: false, + }, + { + usage: quotaUsage, + origin: "http://advanced.search.xyz.com", + cookies: 3, + persisted: true, + }, + { + usage: quotaUsage, + origin: "http://xyz.com", + cookies: 1, + persisted: false, + }, + ]; + await addTestData(testData); + + let updatedPromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatedPromise; + await openSiteDataSettingsDialog(); + let win = gBrowser.selectedBrowser.contentWindow; + let dialogFrame = win.gSubDialog._topDialog._frame; + let frameDoc = dialogFrame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is( + siteItems.length, + 1, + "Should group sites across scheme, port and origin attributes" + ); + + let columns = siteItems[0].querySelectorAll(".item-box > label"); + + let expected = "xyz.com"; + is(columns[0].value, expected, "Should group and list sites by host"); + + let cookieCount = testData.reduce((count, { cookies }) => count + cookies, 0); + is( + columns[1].value, + cookieCount.toString(), + "Should group cookies across scheme, port and origin attributes" + ); + + let [value, unit] = DownloadUtils.convertByteUnits(quotaUsage * 4); + let l10nAttributes = frameDoc.l10n.getAttributes(columns[2]); + is( + l10nAttributes.id, + "site-storage-persistent", + "Should show the site as persistent if one origin is persistent." + ); + // The shown quota can be slightly larger than the raw data we put in (though it should + // never be smaller), but that doesn't really matter to us since we only want to test that + // the site data dialog accumulates this into a single column. + ok( + parseFloat(l10nAttributes.args.value) >= parseFloat(value), + "Should show the correct accumulated quota size." + ); + is( + l10nAttributes.args.unit, + unit, + "Should show the correct quota size unit." + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test sorting +add_task(async function test_sorting() { + let testData = [ + { + usage: 1024, + origin: "https://account.xyz.com", + cookies: 6, + persisted: true, + }, + { + usage: 1024 * 2, + origin: "https://books.foo.com", + cookies: 0, + persisted: false, + }, + { + usage: 1024 * 3, + origin: "http://cinema.bar.com", + cookies: 3, + persisted: true, + }, + { + usage: 1024 * 3, + origin: "http://vod.bar.com", + cookies: 2, + persisted: false, + }, + ]; + + await addTestData(testData); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let dialog = content.gSubDialog._topDialog; + let dialogFrame = dialog._frame; + let frameDoc = dialogFrame.contentDocument; + let hostCol = frameDoc.getElementById("hostCol"); + let usageCol = frameDoc.getElementById("usageCol"); + let cookiesCol = frameDoc.getElementById("cookiesCol"); + let sitesList = frameDoc.getElementById("sitesList"); + + function getHostOrder() { + let siteItems = sitesList.getElementsByTagName("richlistitem"); + return Array.from(siteItems).map(item => item.getAttribute("host")); + } + + // Test default sorting by usage, descending. + Assert.deepEqual( + getHostOrder(), + ["bar.com", "foo.com", "xyz.com"], + "Has sorted descending by usage" + ); + + // Test sorting on the usage column + usageCol.click(); + Assert.deepEqual( + getHostOrder(), + ["xyz.com", "foo.com", "bar.com"], + "Has sorted ascending by usage" + ); + usageCol.click(); + Assert.deepEqual( + getHostOrder(), + ["bar.com", "foo.com", "xyz.com"], + "Has sorted descending by usage" + ); + + // Test sorting on the host column + hostCol.click(); + Assert.deepEqual( + getHostOrder(), + ["bar.com", "foo.com", "xyz.com"], + "Has sorted ascending by base domain" + ); + hostCol.click(); + Assert.deepEqual( + getHostOrder(), + ["xyz.com", "foo.com", "bar.com"], + "Has sorted descending by base domain" + ); + + // Test sorting on the cookies column + cookiesCol.click(); + Assert.deepEqual( + getHostOrder(), + ["foo.com", "bar.com", "xyz.com"], + "Has sorted ascending by cookies" + ); + cookiesCol.click(); + Assert.deepEqual( + getHostOrder(), + ["xyz.com", "bar.com", "foo.com"], + "Has sorted descending by cookies" + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test single entry removal +add_task(async function test_single_entry_removal() { + let testData = await addTestData([ + { + usage: 1024, + origin: "https://xyz.com", + cookies: 6, + persisted: true, + }, + { + usage: 1024 * 3, + origin: "http://bar.com", + cookies: 2, + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let dialog = content.gSubDialog._topDialog; + let dialogFrame = dialog._frame; + let frameDoc = dialogFrame.contentDocument; + + let sitesList = frameDoc.getElementById("sitesList"); + let host = testData[0]; + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + sitesList.addItemToSelection(site); + frameDoc.getElementById("removeSelected").doCommand(); + let saveChangesButton = frameDoc.querySelector("dialog").getButton("accept"); + let dialogOpened = BrowserTestUtils.promiseAlertDialogOpen( + null, + REMOVE_DIALOG_URL + ); + setTimeout(() => saveChangesButton.doCommand(), 0); + let dialogWin = await dialogOpened; + let rootElement = dialogWin.document.getElementById( + "SiteDataRemoveSelectedDialog" + ); + is(rootElement.classList.length, 1, "There should only be one class set"); + is( + rootElement.classList[0], + "single-entry", + "The only class set should be single-entry (to hide the list)" + ); + let description = dialogWin.document.getElementById("removing-description"); + is( + description.getAttribute("data-l10n-id"), + "site-data-removing-single-desc", + "The description for single site should be selected" + ); + + let removalList = dialogWin.document.getElementById("removalList"); + is( + BrowserTestUtils.is_visible(removalList), + false, + "The removal list should be invisible" + ); + let removeButton = dialogWin.document + .querySelector("dialog") + .getButton("accept"); + let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + updatePromise = promiseSiteDataManagerSitesUpdated(); + removeButton.doCommand(); + await dialogClosed; + await updatePromise; + await openSiteDataSettingsDialog(); + + dialog = content.gSubDialog._topDialog; + dialogFrame = dialog._frame; + frameDoc = dialogFrame.contentDocument; + assertSitesListed(frameDoc, testData.slice(1)); + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js b/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js new file mode 100644 index 0000000000..5ce9d7e1e1 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js @@ -0,0 +1,119 @@ +"use strict"; + +// Test selecting and removing partial sites +add_task(async function () { + await SiteDataTestUtils.clear(); + + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://127.0.0.1", + persisted: false, + }, + { + usage: 1024 * 4, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024 * 3, + origin: "http://email.bar.com", + persisted: false, + }, + { + usage: 1024 * 2, + origin: "https://s3-us-west-2.amazonaws.com", + persisted: true, + }, + { + usage: 1024 * 6, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024 * 5, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024 * 5, + origin: "https://example.com", + persisted: false, + }, + { + usage: 1024 * 5, + origin: "https://example.net", + persisted: false, + }, + ]); + + // Align the order of test hosts with the order of the site data table. + hosts.sort(); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let doc = gBrowser.selectedBrowser.contentDocument; + + // Test the initial state + assertSitesListed(doc, hosts); + let win = gBrowser.selectedBrowser.contentWindow; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + is( + removeBtn.disabled, + true, + "Should start with disabled removeSelected button" + ); + + let hostCol = frameDoc.getElementById("hostCol"); + hostCol.click(); + + let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + let settingsDialogClosePromise = promiseSettingsDialogClose(); + + // Select some sites to remove. + let sitesList = frameDoc.getElementById("sitesList"); + hosts.slice(0, 2).forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + sitesList.addItemToSelection(site); + }); + + is(removeBtn.disabled, false, "Should enable the removeSelected button"); + removeBtn.doCommand(); + is(sitesList.selectedIndex, 0, "Should select next item"); + assertSitesListed(doc, hosts.slice(2)); + + // Select some other sites to remove with Delete. + hosts.slice(2, 4).forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + sitesList.addItemToSelection(site); + }); + + is(removeBtn.disabled, false, "Should enable the removeSelected button"); + // Move the focus from the search box to the list + sitesList.focus(); + EventUtils.synthesizeKey("VK_DELETE"); + is(sitesList.selectedIndex, 0, "Should select next item"); + assertSitesListed(doc, hosts.slice(4)); + + updatePromise = promiseSiteDataManagerSitesUpdated(); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + saveBtn.doCommand(); + + await removeDialogOpenPromise; + await settingsDialogClosePromise; + + await updatePromise; + await openSiteDataSettingsDialog(); + + assertSitesListed(doc, hosts.slice(4)); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/head.js b/browser/components/preferences/tests/siteData/head.js new file mode 100644 index 0000000000..01f56d879d --- /dev/null +++ b/browser/components/preferences/tests/siteData/head.js @@ -0,0 +1,280 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_QUOTA_USAGE_HOST = "example.com"; +const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST; +const TEST_QUOTA_USAGE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_QUOTA_USAGE_ORIGIN + ) + "/site_data_test.html"; +const TEST_OFFLINE_HOST = "example.org"; +const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST; +const TEST_OFFLINE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_OFFLINE_ORIGIN + ) + "/offline/offline.html"; +const TEST_SERVICE_WORKER_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_OFFLINE_ORIGIN + ) + "/service_worker_test.html"; + +const REMOVE_DIALOG_URL = + "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml"; + +ChromeUtils.defineESModuleGetters(this, { + SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "serviceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +function promiseSiteDataManagerSitesUpdated() { + return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true); +} + +function is_element_visible(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(!BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function is_element_hidden(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function promiseLoadSubDialog(aURL) { + return new Promise((resolve, reject) => { + content.gSubDialog._dialogStack.addEventListener( + "dialogopen", + function dialogopen(aEvent) { + if ( + aEvent.detail.dialog._frame.contentWindow.location == "about:blank" + ) { + return; + } + content.gSubDialog._dialogStack.removeEventListener( + "dialogopen", + dialogopen + ); + + is( + aEvent.detail.dialog._frame.contentWindow.location.toString(), + aURL, + "Check the proper URL is loaded" + ); + + // Check visibility + is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible"); + + // Check that stylesheets were injected + let expectedStyleSheetURLs = + aEvent.detail.dialog._injectedStyleSheets.slice(0); + for (let styleSheet of aEvent.detail.dialog._frame.contentDocument + .styleSheets) { + let i = expectedStyleSheetURLs.indexOf(styleSheet.href); + if (i >= 0) { + info("found " + styleSheet.href); + expectedStyleSheetURLs.splice(i, 1); + } + } + is( + expectedStyleSheetURLs.length, + 0, + "All expectedStyleSheetURLs should have been found" + ); + + // Wait for the next event tick to make sure the remaining part of the + // testcase runs after the dialog gets ready for input. + executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow)); + } + ); + }); +} + +function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) { + return new Promise(resolve => { + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + openPreferences(aPane); + let newTabBrowser = gBrowser.selectedBrowser; + + newTabBrowser.addEventListener( + "Initialized", + function () { + newTabBrowser.contentWindow.addEventListener( + "load", + async function () { + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + await finalPrefPaneLoaded; + if (!aOptions || !aOptions.leaveOpen) { + gBrowser.removeCurrentTab(); + } + resolve({ selectedPane }); + }, + { once: true } + ); + }, + { capture: true, once: true } + ); + }); +} + +function openSiteDataSettingsDialog() { + let doc = gBrowser.selectedBrowser.contentDocument; + let settingsBtn = doc.getElementById("siteDataSettings"); + let dialogOverlay = content.gSubDialog._preloadDialog._overlay; + let dialogLoadPromise = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml" + ); + let dialogInitPromise = TestUtils.topicObserved( + "sitedata-settings-init", + () => true + ); + let fullyLoadPromise = Promise.all([ + dialogLoadPromise, + dialogInitPromise, + ]).then(() => { + is_element_visible(dialogOverlay, "The Settings dialog should be visible"); + }); + settingsBtn.doCommand(); + return fullyLoadPromise; +} + +function promiseSettingsDialogClose() { + return new Promise(resolve => { + let win = gBrowser.selectedBrowser.contentWindow; + let dialogOverlay = win.gSubDialog._topDialog._overlay; + let dialogWin = win.gSubDialog._topDialog._frame.contentWindow; + dialogWin.addEventListener( + "unload", + function unload() { + if ( + dialogWin.document.documentURI === + "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml" + ) { + is_element_hidden( + dialogOverlay, + "The Settings dialog should be hidden" + ); + resolve(); + } + }, + { once: true } + ); + }); +} + +function assertSitesListed(doc, hosts) { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + let removeAllBtn = frameDoc.getElementById("removeAll"); + let sitesList = frameDoc.getElementById("sitesList"); + let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length; + is(totalSitesNumber, hosts.length, "Should list the right sites number"); + hosts.forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + ok(site, `Should list the site of ${host}`); + }); + is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button"); +} + +// Counter used by addTestData to generate unique cookie names across function +// calls. +let cookieID = 0; + +async function addTestData(data) { + let hosts = new Set(); + + for (let site of data) { + is( + typeof site.origin, + "string", + "Passed an origin string into addTestData." + ); + if (site.persisted) { + await SiteDataTestUtils.persist(site.origin); + } + + if (site.usage) { + await SiteDataTestUtils.addToIndexedDB(site.origin, site.usage); + } + + for (let i = 0; i < (site.cookies || 0); i++) { + SiteDataTestUtils.addToCookies({ + origin: site.origin, + name: `cookie${cookieID++}`, + }); + } + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + site.origin + ); + + hosts.add(principal.baseDomain || principal.host); + } + + return Array.from(hosts); +} + +function promiseCookiesCleared() { + return TestUtils.topicObserved("cookie-changed", (subj, data) => { + return data === "cleared"; + }); +} + +async function loadServiceWorkerTestPage(url) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await TestUtils.waitForCondition(() => { + return SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => + content.document.body.getAttribute( + "data-test-service-worker-registered" + ) === "true" + ); + }, `Fail to load service worker test ${url}`); + BrowserTestUtils.removeTab(tab); +} + +function promiseServiceWorkersCleared() { + return TestUtils.waitForCondition(() => { + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + if (!serviceWorkers.length) { + ok(true, "Cleared all service workers"); + return true; + } + return false; + }, "Should clear all service workers"); +} + +function promiseServiceWorkerRegisteredFor(url) { + return TestUtils.waitForCondition(() => { + try { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(url); + let sw = serviceWorkerManager.getRegistrationByPrincipal( + principal, + principal.spec + ); + if (sw) { + ok(true, `Found the service worker registered for ${url}`); + return true; + } + } catch (e) {} + return false; + }, `Should register service worker for ${url}`); +} diff --git a/browser/components/preferences/tests/siteData/offline/manifest.appcache b/browser/components/preferences/tests/siteData/offline/manifest.appcache new file mode 100644 index 0000000000..a9287c64e6 --- /dev/null +++ b/browser/components/preferences/tests/siteData/offline/manifest.appcache @@ -0,0 +1,3 @@ +CACHE MANIFEST +# V1 +offline.html diff --git a/browser/components/preferences/tests/siteData/offline/offline.html b/browser/components/preferences/tests/siteData/offline/offline.html new file mode 100644 index 0000000000..f76b8a2bce --- /dev/null +++ b/browser/components/preferences/tests/siteData/offline/offline.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html manifest="manifest.appcache">> + <head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="public" /> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"> + + </head> + + <body> + <h1>Set up offline appcache Test</h1> + </body> +</html> diff --git a/browser/components/preferences/tests/siteData/service_worker_test.html b/browser/components/preferences/tests/siteData/service_worker_test.html new file mode 100644 index 0000000000..56f5173481 --- /dev/null +++ b/browser/components/preferences/tests/siteData/service_worker_test.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="public" /> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"> + + <title>Service Worker Test</title> + + </head> + + <body> + <h1>Service Worker Test</h1> + <script type="text/javascript"> + navigator.serviceWorker.register("service_worker_test.js") + .then(regis => document.body.setAttribute("data-test-service-worker-registered", "true")); + </script> + </body> +</html> diff --git a/browser/components/preferences/tests/siteData/service_worker_test.js b/browser/components/preferences/tests/siteData/service_worker_test.js new file mode 100644 index 0000000000..2aba167d18 --- /dev/null +++ b/browser/components/preferences/tests/siteData/service_worker_test.js @@ -0,0 +1 @@ +// empty worker, always succeed! diff --git a/browser/components/preferences/tests/siteData/site_data_test.html b/browser/components/preferences/tests/siteData/site_data_test.html new file mode 100644 index 0000000000..758106b0a5 --- /dev/null +++ b/browser/components/preferences/tests/siteData/site_data_test.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="public" /> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"> + + <title>Site Data Test</title> + + </head> + + <body> + <h1>Site Data Test</h1> + <script type="text/javascript"> + let request = indexedDB.open("TestDatabase", 1); + request.onupgradeneeded = function(e) { + let db = e.target.result; + db.createObjectStore("TestStore", { keyPath: "id" }); + }; + request.onsuccess = function(e) { + let db = e.target.result; + let tx = db.transaction("TestStore", "readwrite"); + let store = tx.objectStore("TestStore"); + tx.oncomplete = () => document.dispatchEvent(new CustomEvent("test-indexedDB-done", {bubbles: true, cancelable: false})); + store.put({ id: "test_id", description: "Site Data Test"}); + }; + </script> + </body> +</html> diff --git a/browser/components/preferences/tests/subdialog.xhtml b/browser/components/preferences/tests/subdialog.xhtml new file mode 100644 index 0000000000..54fa88c25d --- /dev/null +++ b/browser/components/preferences/tests/subdialog.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> + +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Sample sub-dialog" style="width: 32em; height: 5em;" + onload="document.getElementById('textbox').focus();"> +<dialog id="subDialog"> + <script> + document.addEventListener("dialogaccept", acceptSubdialog); + function acceptSubdialog() { + window.arguments[0].acceptCount++; + } + </script> + + <description id="desc">A sample sub-dialog for testing</description> + + <html:input id="textbox" value="Default text" /> + + <separator class="thin"/> + + <button oncommand="window.close();" label="Close" /> + +</dialog> +</window> diff --git a/browser/components/preferences/tests/subdialog2.xhtml b/browser/components/preferences/tests/subdialog2.xhtml new file mode 100644 index 0000000000..9ae04d5675 --- /dev/null +++ b/browser/components/preferences/tests/subdialog2.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> + +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Sample sub-dialog #2" style="width: 32em; height: 5em;" + onload="document.getElementById('textbox').focus();"> +<dialog id="subDialog"> + <script> + document.addEventListener("dialogaccept", acceptSubdialog); + function acceptSubdialog() { + window.arguments[0].acceptCount++; + } + </script> + + <description id="desc">A sample sub-dialog for testing</description> + + <html:input id="textbox" value="Default text" /> + + <separator class="thin"/> + + <button oncommand="window.close();" label="Close" /> + +</dialog> +</window> diff --git a/browser/components/preferences/web-appearance-dark.svg b/browser/components/preferences/web-appearance-dark.svg new file mode 100644 index 0000000000..1f4c1d81c2 --- /dev/null +++ b/browser/components/preferences/web-appearance-dark.svg @@ -0,0 +1,17 @@ +<!-- 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/. --> +<svg viewBox="0 0 54 42" xmlns="http://www.w3.org/2000/svg"> +<rect width="54" height="42" fill="white"/> +<rect width="54" height="11" fill="#F9F9FB"/> +<rect width="54" height="5" fill="#F0F0F4"/> +<rect x="5" y="2" width="12" height="2" rx="1" fill="#5B5B66"/> +<rect x="50" y="7" width="2" height="2" rx="1" fill="#5B5B66"/> +<rect x="2" y="7" width="2" height="2" rx="1" fill="#5B5B66"/> +<rect x="6" y="7" width="2" height="2" rx="1" fill="#5B5B66"/> +<rect x="10" y="7" width="38" height="2" rx="1" fill="#5B5B66"/> +<rect y="11" width="54" height="31" fill="#42414D"/> +<rect x="4" y="28" width="27" height="2" rx="1" fill="#CFCFD8"/> +<rect x="4" y="32" width="16" height="2" rx="1" fill="#CFCFD8"/> +<rect x="4" y="36" width="31" height="2" rx="1" fill="#CFCFD8"/> +</svg> diff --git a/browser/components/preferences/web-appearance-light.svg b/browser/components/preferences/web-appearance-light.svg new file mode 100644 index 0000000000..6d6af7e397 --- /dev/null +++ b/browser/components/preferences/web-appearance-light.svg @@ -0,0 +1,17 @@ +<!-- 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/. --> +<svg viewBox="0 0 54 42" xmlns="http://www.w3.org/2000/svg"> +<rect width="54" height="42" fill="white"/> +<rect width="54" height="11" fill="#F9F9FB"/> +<rect width="54" height="5" fill="#F0F0F4"/> +<rect x="5" y="2" width="12" height="2" rx="1" fill="#5B5B66"/> +<rect x="50" y="7" width="2" height="2" rx="1" fill="#5B5B66"/> +<rect x="2" y="7" width="2" height="2" rx="1" fill="#5B5B66"/> +<rect x="6" y="7" width="2" height="2" rx="1" fill="#5B5B66"/> +<rect x="10" y="7" width="38" height="2" rx="1" fill="#5B5B66"/> +<rect y="11" width="54" height="31" fill="white"/> +<rect x="4" y="28" width="27" height="2" rx="1" fill="#5B5B66"/> +<rect x="4" y="32" width="16" height="2" rx="1" fill="#5B5B66"/> +<rect x="4" y="36" width="31" height="2" rx="1" fill="#5B5B66"/> +</svg> |