diff options
Diffstat (limited to 'browser/components/preferences')
176 files changed, 36144 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..42f6dc2da1 --- /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-button"/> +</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..fd9393ab9d --- /dev/null +++ b/browser/components/preferences/containers.js @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { ContextualIdentityService } = ChromeUtils.import( + "resource://gre/modules/ContextualIdentityService.jsm" +); + +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.setAttribute("width", 24); + userContextIcon.setAttribute("height", 24); + 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.setAttribute("flex", 1); + label.setAttribute("crop", "end"); + label.textContent = ContextualIdentityService.getUserContextLabel( + container.userContextId + ); + 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-preferences-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..8d5f5bfe3e --- /dev/null +++ b/browser/components/preferences/dialogs/addEngine.css @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +input { + -moz-box-flex: 1; +} + +hbox { + width: 100%; +} + +#engineNameLabel, +#engineUrlLabel, +#engineAliasLabel { + /* Align the labels with the inputs */ + margin-inline-start: 4px; +} + +#engineUrl { + /* Full URLs should always be displayed as LTR */ + direction: ltr; + text-align: match-parent; +} diff --git a/browser/components/preferences/dialogs/addEngine.js b/browser/components/preferences/dialogs/addEngine.js new file mode 100644 index 0000000000..1faf8622b3 --- /dev/null +++ b/browser/components/preferences/dialogs/addEngine.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../main.js */ + +let gAddEngineDialog = { + _form: null, + _name: null, + _alias: null, + + onLoad() { + document.mozSubdialogReady = this.init(); + }, + + async init() { + this._dialog = document.querySelector("dialog"); + this._form = document.getElementById("addEngineForm"); + this._name = document.getElementById("engineName"); + this._alias = document.getElementById("engineAlias"); + + this._name.addEventListener("input", this.onNameInput.bind(this)); + this._alias.addEventListener("input", this.onAliasInput.bind(this)); + this._form.addEventListener("input", this.onFormInput.bind(this)); + + document.addEventListener("dialogaccept", this.onAddEngine.bind(this)); + }, + + async onAddEngine(event) { + let url = document + .getElementById("engineUrl") + .value.replace(/%s/, "{searchTerms}"); + await Services.search.wrappedJSObject.addUserEngine( + this._name.value, + url, + this._alias.value + ); + }, + + async onNameInput() { + if (this._name.value) { + let engine = Services.search.getEngineByName(this._name.value); + let validity = engine + ? document.getElementById("engineNameExists").textContent + : ""; + this._name.setCustomValidity(validity); + } + }, + + async onAliasInput() { + let validity = ""; + if (this._alias.value) { + let engine = await Services.search.getEngineByAlias(this._alias.value); + if (engine) { + engine = document.getElementById("engineAliasExists").textContent; + } + } + this._alias.setCustomValidity(validity); + }, + + async onFormInput() { + this._dialog.setAttribute( + "buttondisabledaccept", + !this._form.checkValidity() + ); + }, +}; + +window.addEventListener("load", () => gAddEngineDialog.onLoad()); diff --git a/browser/components/preferences/dialogs/addEngine.xhtml b/browser/components/preferences/dialogs/addEngine.xhtml new file mode 100644 index 0000000000..52c425733f --- /dev/null +++ b/browser/components/preferences/dialogs/addEngine.xhtml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/addEngine.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="add-engine-window" + 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..1e4da65843 --- /dev/null +++ b/browser/components/preferences/dialogs/applicationManager.js @@ -0,0 +1,131 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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]; + Services.scriptloader.loadSubScript( + "chrome://browser/content/preferences/main.js", + window + ); + + document.addEventListener("dialogaccept", function() { + gAppManagerDialog.onOK(); + }); + + 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..e442c03eca --- /dev/null +++ b/browser/components/preferences/dialogs/applicationManager.xhtml @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gAppManagerDialog.onLoad();" + data-l10n-id="app-manager-window" + 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..bbf6a5f4c2 --- /dev/null +++ b/browser/components/preferences/dialogs/blocklists.js @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +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..06948335f4 --- /dev/null +++ b/browser/components/preferences/dialogs/blocklists.xhtml @@ -0,0 +1,53 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window id="BlocklistsDialog" + data-l10n-id="blocklist-window" + 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="" flex="1" sortable="false" + type="checkbox"/> + <treecol id="listCol" data-l10n-id="blocklist-treehead-list" flex="80" + sortable="false"/> + </treecols> + <treechildren/> + </tree> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/browserLanguages.js b/browser/components/preferences/dialogs/browserLanguages.js new file mode 100644 index 0000000000..93be9203e8 --- /dev/null +++ b/browser/components/preferences/dialogs/browserLanguages.js @@ -0,0 +1,647 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// This is exported by preferences.js but we can't import that in a subdialog. +let { getAvailableLocales } = window.top; + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AddonRepository", + "resource://gre/modules/addons/AddonRepository.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "RemoteSettings", + "resource://services-settings/remote-settings.js" +); +ChromeUtils.defineModuleGetter( + this, + "SelectionChangedMenulist", + "resource:///modules/SelectionChangedMenulist.jsm" +); + +document + .getElementById("BrowserLanguagesDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); + +/* This dialog provides an interface for managing what language the browser is + * displayed in. + * + * There is a list of "requested" locales and a list of "available" locales. The + * requested locales must be installed and enabled. Available locales could be + * installed and enabled, or fetched from the AMO language tools API. + * + * If a langpack is disabled, there is no way to determine what locale it is for and + * it will only be listed as available if that locale is also available on AMO and + * the user has opted to search for more languages. + */ + +async function installFromUrl(url, hash, callback) { + let telemetryInfo = { + source: "about:preferences", + }; + let install = await AddonManager.getInstallForURL(url, { + hash, + telemetryInfo, + }); + if (callback) { + callback(install.installId.toString()); + } + await install.install(); + return install.addon; +} + +async function dictionaryIdsForLocale(locale) { + let entries = await RemoteSettings("language-dictionaries").get({ + filters: { id: locale }, + }); + if (entries.length) { + return entries[0].dictionaries; + } + return []; +} + +class OrderedListBox { + constructor({ + richlistbox, + upButton, + downButton, + removeButton, + onRemove, + onReorder, + }) { + this.richlistbox = richlistbox; + this.upButton = upButton; + this.downButton = downButton; + this.removeButton = removeButton; + this.onRemove = onRemove; + this.onReorder = onReorder; + + this.items = []; + + this.richlistbox.addEventListener("select", () => this.setButtonState()); + this.upButton.addEventListener("command", () => this.moveUp()); + this.downButton.addEventListener("command", () => this.moveDown()); + this.removeButton.addEventListener("command", () => this.removeItem()); + } + + get selectedItem() { + return this.items[this.richlistbox.selectedIndex]; + } + + setButtonState() { + let { upButton, downButton, removeButton } = this; + let { selectedIndex, itemCount } = this.richlistbox; + upButton.disabled = selectedIndex <= 0; + downButton.disabled = selectedIndex == itemCount - 1; + removeButton.disabled = itemCount <= 1 || !this.selectedItem.canRemove; + } + + moveUp() { + let { selectedIndex } = this.richlistbox; + if (selectedIndex == 0) { + return; + } + let { items } = this; + let selectedItem = items[selectedIndex]; + let prevItem = items[selectedIndex - 1]; + items[selectedIndex - 1] = items[selectedIndex]; + items[selectedIndex] = prevItem; + let prevEl = document.getElementById(prevItem.id); + let selectedEl = document.getElementById(selectedItem.id); + this.richlistbox.insertBefore(selectedEl, prevEl); + this.richlistbox.ensureElementIsVisible(selectedEl); + this.setButtonState(); + + this.onReorder(); + } + + moveDown() { + let { selectedIndex } = this.richlistbox; + if (selectedIndex == this.items.length - 1) { + return; + } + let { items } = this; + let selectedItem = items[selectedIndex]; + let nextItem = items[selectedIndex + 1]; + items[selectedIndex + 1] = items[selectedIndex]; + items[selectedIndex] = nextItem; + let nextEl = document.getElementById(nextItem.id); + let selectedEl = document.getElementById(selectedItem.id); + this.richlistbox.insertBefore(nextEl, selectedEl); + this.richlistbox.ensureElementIsVisible(selectedEl); + this.setButtonState(); + + this.onReorder(); + } + + removeItem() { + let { selectedIndex } = this.richlistbox; + + if (selectedIndex == -1) { + return; + } + + let [item] = this.items.splice(selectedIndex, 1); + this.richlistbox.selectedItem.remove(); + this.richlistbox.selectedIndex = Math.min( + selectedIndex, + this.richlistbox.itemCount - 1 + ); + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + this.onRemove(item); + } + + setItems(items) { + this.items = items; + this.populate(); + this.setButtonState(); + } + + /** + * Add an item to the top of the ordered list. + * + * @param {object} item The item to insert. + */ + addItem(item) { + this.items.unshift(item); + this.richlistbox.insertBefore( + this.createItem(item), + this.richlistbox.firstElementChild + ); + this.richlistbox.selectedIndex = 0; + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + } + + populate() { + this.richlistbox.textContent = ""; + + let frag = document.createDocumentFragment(); + for (let item of this.items) { + frag.appendChild(this.createItem(item)); + } + this.richlistbox.appendChild(frag); + + this.richlistbox.selectedIndex = 0; + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + } + + createItem({ id, label, value }) { + let listitem = document.createXULElement("richlistitem"); + listitem.id = id; + listitem.setAttribute("value", value); + + let labelEl = document.createXULElement("label"); + labelEl.textContent = label; + listitem.appendChild(labelEl); + + return listitem; + } +} + +class SortedItemSelectList { + constructor({ menulist, button, onSelect, onChange, compareFn }) { + this.menulist = menulist; + this.popup = menulist.menupopup; + this.button = button; + this.compareFn = compareFn; + 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); + }); + } + + 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; + } +} + +async function getLocaleDisplayInfo(localeCodes) { + let availableLocales = new Set(await getAvailableLocales()); + let packagedLocales = new Set(Services.locale.packagedLocales); + let localeNames = Services.intl.getLocaleDisplayNames(undefined, localeCodes); + return localeCodes.map((code, i) => { + return { + id: "locale-" + code, + label: localeNames[i], + value: code, + canRemove: !packagedLocales.has(code), + installed: availableLocales.has(code), + }; + }); +} + +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 = { + telemetryId: null, + accepted: false, + _availableLocales: null, + _selectedLocales: null, + selectedLocales: 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 + ); + }, + + beforeAccept() { + this.selected = this.getSelectedLocales(); + this.accepted = true; + }, + + async onLoad() { + document + .getElementById("BrowserLanguagesDialog") + .addEventListener("beforeaccept", () => this.beforeAccept()); + // Maintain the previously selected locales even if we cancel out. + let { telemetryId, selected, search } = window.arguments[0]; + this.telemetryId = telemetryId; + this.selectedLocales = selected; + + // 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 = + this.selectedLocales || Services.locale.appLocalesAsBCP47; + let selectedLocaleSet = new Set(selectedLocales); + let available = await getAvailableLocales(); + let availableSet = new Set(available); + + // Filter selectedLocales since the user may select a locale when it is + // available and then disable it. + selectedLocales = selectedLocales.filter(locale => + availableSet.has(locale) + ); + // Nothing in available should be in selectedSet. + available = available.filter(locale => !selectedLocaleSet.has(locale)); + + await this.initSelectedLocales(selectedLocales); + await this.initAvailableLocales(available, search); + + this.initialized = true; + }, + + async initSelectedLocales(selectedLocales) { + this._selectedLocales = 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._selectedLocales.setItems(await getLocaleDisplayInfo(selectedLocales)); + }, + + async initAvailableLocales(available, search) { + this._availableLocales = 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._availableLocales.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 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._availableLocales.items; + items.pop(); + items = items.concat(availableItems); + + // Update the dropdown and enable it again. + this._availableLocales.setItems(items); + this._availableLocales.enableWithMessageId( + "browser-languages-select-language" + ); + }, + + 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._availableLocales.setItems(items); + }, + + async availableLanguageSelected(item) { + if ((await 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(); + } + }, + + async requestLocalLanguage(item, available) { + this._selectedLocales.addItem(item); + let selectedCount = this._selectedLocales.items.length; + let availableCount = (await getAvailableLocales()).length; + if (selectedCount == availableCount) { + // Remove the installed label, they're all installed. + this._availableLocales.items.shift(); + this._availableLocales.setItems(this._availableLocales.items); + } + // The label isn't always reset when the selected item is removed, so set it again. + this._availableLocales.enableWithMessageId( + "browser-languages-select-language" + ); + }, + + async requestRemoteLanguage(item) { + this._availableLocales.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._selectedLocales.addItem(item); + this._availableLocales.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); + }, + + 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) { + Cu.reportError(e); + } + }, + + showError() { + document.getElementById("warning-message").hidden = false; + this._availableLocales.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; + }, + + getSelectedLocales() { + return this._selectedLocales.items.map(item => item.value); + }, + + async selectedLocaleRemoved(item) { + this.recordTelemetry("remove"); + + this._availableLocales.addItem(item); + + // If the item we added is at the top of the list, it needs the label. + if (this._availableLocales.items[0] == item) { + this._availableLocales.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..ef2f11c289 --- /dev/null +++ b/browser/components/preferences/dialogs/browserLanguages.xhtml @@ -0,0 +1,55 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="browser-languages-window" + data-l10n-attrs="title, style" + onload="gBrowserLanguagesDialog.onLoad();"> +<dialog id="BrowserLanguagesDialog" + buttons="accept,cancel,help" + helpTopic="change-language"> + + <linkset> + <html:link rel="localization" href="branding/brand.ftl"/> + <html:link rel="localization" href="browser/preferences/languages.ftl"/> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js"/> + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://browser/content/preferences/dialogs/browserLanguages.js"/> + + <description data-l10n-id="browser-languages-description"/> + + <box flex="1" style="display: grid; grid-template-rows: 1fr auto; grid-template-columns: 1fr auto;"> + <richlistbox id="selectedLocales"/> + <vbox> + <button id="up" class="action-button" disabled="true" data-l10n-id="languages-customize-moveup"/> + <button id="down" class="action-button" disabled="true" data-l10n-id="languages-customize-movedown"/> + <button id="remove" class="action-button" disabled="true" data-l10n-id="languages-customize-remove"/> + </vbox> + + <menulist id="availableLocales" + class="available-locales-list" + data-l10n-id="browser-languages-select-language" + data-l10n-attrs="placeholder,label"> + <menupopup/> + </menulist> + <button id="add" + class="add-browser-language action-button" + data-l10n-id="languages-customize-add" + disabled="true"/> + </box> + <hbox id="warning-message" class="message-bar message-bar-warning" hidden="true"> + <image class="message-bar-icon"/> + <description class="message-bar-description" data-l10n-id="browser-languages-error"/> + </hbox> +</dialog> +</window> diff --git a/browser/components/preferences/dialogs/clearSiteData.css b/browser/components/preferences/dialogs/clearSiteData.css new file mode 100644 index 0000000000..144408ca0b --- /dev/null +++ b/browser/components/preferences/dialogs/clearSiteData.css @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.options-container { + background-color: var(--in-content-box-background); + border: 1px solid var(--in-content-box-border-color); + border-radius: 2px; + color: var(--in-content-text-color); + padding: 0.5em; +} + +.option { + padding-bottom: 8px; +} + +.option-description { + color: var(--in-content-deemphasized-text); + margin-top: -0.5em !important; +} diff --git a/browser/components/preferences/dialogs/clearSiteData.js b/browser/components/preferences/dialogs/clearSiteData.js new file mode 100644 index 0000000000..ca0d65d86d --- /dev/null +++ b/browser/components/preferences/dialogs/clearSiteData.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { SiteDataManager } = ChromeUtils.import( + "resource:///modules/SiteDataManager.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm" +); + +var gClearSiteDataDialog = { + _clearSiteDataCheckbox: null, + _clearCacheCheckbox: null, + + onLoad() { + document.mozSubdialogReady = this.init(); + }, + + async init() { + this._dialog = document.querySelector("dialog"); + this._clearSiteDataCheckbox = document.getElementById("clearSiteData"); + this._clearCacheCheckbox = document.getElementById("clearCache"); + + // We'll block init() on this because the result values may impact + // subdialog sizing. + await Promise.all([ + SiteDataManager.getTotalUsage().then(bytes => { + let [amount, unit] = DownloadUtils.convertByteUnits(bytes); + document.l10n.setAttributes( + this._clearSiteDataCheckbox, + "clear-site-data-cookies-with-data", + { amount, unit } + ); + }), + SiteDataManager.getCacheSize().then(bytes => { + let [amount, unit] = DownloadUtils.convertByteUnits(bytes); + document.l10n.setAttributes( + this._clearCacheCheckbox, + "clear-site-data-cache-with-data", + { amount, unit } + ); + }), + ]); + await document.l10n.translateElements([ + this._clearCacheCheckbox, + this._clearSiteDataCheckbox, + ]); + + document.addEventListener("dialogaccept", event => this.onClear(event)); + + this._clearSiteDataCheckbox.addEventListener("command", e => + this.onCheckboxCommand(e) + ); + this._clearCacheCheckbox.addEventListener("command", e => + this.onCheckboxCommand(e) + ); + }, + + onCheckboxCommand(event) { + this._dialog.setAttribute( + "buttondisabledaccept", + !(this._clearSiteDataCheckbox.checked || this._clearCacheCheckbox.checked) + ); + }, + + onClear(event) { + let clearSiteData = this._clearSiteDataCheckbox.checked; + let clearCache = this._clearCacheCheckbox.checked; + + if (clearSiteData) { + // Ask for confirmation before clearing site data + if (!SiteDataManager.promptSiteDataRemoval(window)) { + clearSiteData = false; + // Prevent closing the dialog when the data removal wasn't allowed. + event.preventDefault(); + } + } + + if (clearSiteData) { + SiteDataManager.removeSiteData(); + } + if (clearCache) { + SiteDataManager.removeCache(); + + // If we're not clearing site data, we need to tell the + // SiteDataManager to signal that it's updating. + if (!clearSiteData) { + SiteDataManager.updateSites(); + } + } + }, +}; + +window.addEventListener("load", () => gClearSiteDataDialog.onLoad()); diff --git a/browser/components/preferences/dialogs/clearSiteData.xhtml b/browser/components/preferences/dialogs/clearSiteData.xhtml new file mode 100644 index 0000000000..b6bab5f861 --- /dev/null +++ b/browser/components/preferences/dialogs/clearSiteData.xhtml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/clearSiteData.css" type="text/css"?> + +<window id="ClearSiteDataDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="clear-site-data-window" + 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..785e90fbcc --- /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..474e2cd5fa --- /dev/null +++ b/browser/components/preferences/dialogs/colors.xhtml @@ -0,0 +1,93 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="colors-window" + 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..7c026e09ad --- /dev/null +++ b/browser/components/preferences/dialogs/connection.js @@ -0,0 +1,649 @@ +/* -*- 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 ../../../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.ftp", type: "string" }, + { id: "network.proxy.ftp_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.ftp", type: "string" }, + { id: "network.proxy.backup.ftp_port", type: "int" }, + { id: "network.proxy.backup.ssl", type: "string" }, + { id: "network.proxy.backup.ssl_port", type: "int" }, + { id: "network.trr.mode", type: "int" }, + { id: "network.trr.uri", type: "string" }, + { id: "network.trr.resolvers", type: "string" }, + { id: "network.trr.custom_uri", type: "string" }, + { id: "doh-rollout.enabled", type: "bool" }, + { id: "doh-rollout.disable-heuristics", type: "bool" }, + { id: "doh-rollout.skipHeuristicsCheck", type: "bool" }, +]); + +window.addEventListener( + "DOMContentLoaded", + () => { + Preferences.get("network.proxy.type").on( + "change", + gConnectionsDialog.proxyTypeChanged.bind(gConnectionsDialog) + ); + Preferences.get("network.proxy.socks_version").on( + "change", + gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog) + ); + + Preferences.get("network.trr.uri").on("change", () => { + gConnectionsDialog.updateDnsOverHttpsUI(); + }); + + Preferences.get("network.trr.resolvers").on("change", () => { + gConnectionsDialog.initDnsOverHttpsUI(); + }); + + // XXX: We can't init the DNS-over-HTTPs UI until the onsyncfrompreference for network.trr.mode + // has been called. The uiReady promise will be resolved after the first call to + // readDnsOverHttpsMode and the subsequent call to initDnsOverHttpsUI has happened. + gConnectionsDialog.uiReady = new Promise(resolve => { + gConnectionsDialog._areTrrPrefsReady = false; + gConnectionsDialog._handleTrrPrefsReady = resolve; + }).then(async () => { + // awaiting this ensures that initDnsOverHttpsUI() is called after + // execution has been returned to the caller of _handleTrrPrefsReady, + // which is the checkbox value reading path. This ensures the checkbox + // gets checked, then initDnsOverHttpsUI() is called, then the uiReady + // promise resolves, preventing intermittent failures in tests. + await gConnectionsDialog.initDnsOverHttpsUI(); + }); + + document + .getElementById("disableProxyExtension") + .addEventListener( + "command", + makeDisableControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).bind( + gConnectionsDialog + ) + ); + gConnectionsDialog.updateProxySettingsUI(); + initializeProxyUI(gConnectionsDialog); + gConnectionsDialog.registerSyncPrefListeners(); + document + .getElementById("ConnectionsDialog") + .addEventListener("beforeaccept", e => + gConnectionsDialog.beforeAccept(e) + ); + }, + { once: true, capture: true } +); + +var gConnectionsDialog = { + beforeAccept(event) { + let dnsOverHttpsResolverChoice = document.getElementById( + "networkDnsOverHttpsResolverChoices" + ).value; + if (dnsOverHttpsResolverChoice == "custom") { + let customValue = document + .getElementById("networkCustomDnsOverHttpsInput") + .value.trim(); + if (customValue) { + Services.prefs.setStringPref("network.trr.uri", customValue); + } else { + Services.prefs.clearUserPref("network.trr.uri"); + } + } else { + Services.prefs.setStringPref( + "network.trr.uri", + dnsOverHttpsResolverChoice + ); + } + + var proxyTypePref = Preferences.get("network.proxy.type"); + if (proxyTypePref.value == 2) { + this.doAutoconfigURLFixup(); + return; + } + + if (proxyTypePref.value != 1) { + return; + } + + var httpProxyURLPref = Preferences.get("network.proxy.http"); + var httpProxyPortPref = Preferences.get("network.proxy.http_port"); + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + + // If the port is 0 and the proxy server is specified, focus on the port and cancel submission. + for (let prefName of ["http", "ssl", "ftp", "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 proxyPrefs = ["ssl", "ftp"]; + 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" + ); + var backupServerURLPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + ); + var backupPortPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + "_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", "ftp", "socks"]; + for (var i = 0; i < proxyPrefs.length; ++i) { + var proxyServerURLPref = Preferences.get( + "network.proxy." + proxyPrefs[i] + ); + var proxyPortPref = Preferences.get( + "network.proxy." + proxyPrefs[i] + "_port" + ); + + // Restore previous per-proxy custom settings, if present. + if (proxyPrefs[i] != "socks" && !shareProxiesPref.value) { + var backupServerURLPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + ); + var backupPortPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + "_port" + ); + if (backupServerURLPref.hasUserValue) { + proxyServerURLPref.value = backupServerURLPref.value; + backupServerURLPref.reset(); + } + if (backupPortPref.hasUserValue) { + proxyPortPref.value = backupPortPref.value; + backupPortPref.reset(); + } + } + + proxyServerURLPref.updateElements(); + proxyPortPref.updateElements(); + let prefIsShared = proxyPrefs[i] != "socks" && shareProxiesPref.value; + proxyServerURLPref.updateControlDisabledState( + proxyTypePref.value != 1 || prefIsShared + ); + proxyPortPref.updateControlDisabledState( + proxyTypePref.value != 1 || prefIsShared + ); + } + var socksVersionPref = Preferences.get("network.proxy.socks_version"); + socksVersionPref.updateControlDisabledState(proxyTypePref.value != 1); + this.updateDNSPref(); + return undefined; + }, + + readProxyProtocolPref(aProtocol, aIsPort) { + if (aProtocol != "socks") { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + var pref = Preferences.get( + "network.proxy.http" + (aIsPort ? "_port" : "") + ); + return pref.value; + } + + var backupPref = Preferences.get( + "network.proxy.backup." + aProtocol + (aIsPort ? "_port" : "") + ); + return backupPref.hasUserValue ? backupPref.value : undefined; + } + return undefined; + }, + + reloadPAC() { + Cc["@mozilla.org/network/protocol-proxy-service;1"] + .getService() + .reloadPAC(); + }, + + doAutoconfigURLFixup() { + var autoURL = document.getElementById("networkProxyAutoconfigURL"); + var autoURLPref = Preferences.get("network.proxy.autoconfig_url"); + try { + autoURLPref.value = autoURL.value = Services.uriFixup.getFixupURIInfo( + autoURL.value + ).preferredURI.spec; + } catch (ex) {} + }, + + sanitizeNoProxiesPref() { + var noProxiesPref = Preferences.get("network.proxy.no_proxies_on"); + // replace substrings of ; and \n with commas if they're neither immediately + // preceded nor followed by a valid separator character + noProxiesPref.value = noProxiesPref.value.replace( + /([^, \n;])[;\n]+(?![,\n;])/g, + "$1," + ); + // replace any remaining ; and \n since some may follow commas, etc. + noProxiesPref.value = noProxiesPref.value.replace(/[;\n]/g, ""); + }, + + readHTTPProxyServer() { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + this.updateProtocolPrefs(); + } + return undefined; + }, + + readHTTPProxyPort() { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + this.updateProtocolPrefs(); + } + return undefined; + }, + + getProxyControls() { + let controlGroup = document.getElementById("networkProxyType"); + return [ + ...controlGroup.querySelectorAll(":scope > radio"), + ...controlGroup.querySelectorAll("label"), + ...controlGroup.querySelectorAll("input"), + ...controlGroup.querySelectorAll("checkbox"), + ...document.querySelectorAll("#networkProxySOCKSVersion > radio"), + ...document.querySelectorAll("#ConnectionsDialogPane > checkbox"), + ]; + }, + + // Update the UI to show/hide the extension controlled message for + // proxy settings. + async updateProxySettingsUI() { + let isLocked = API_PROXY_PREFS.some(pref => + Services.prefs.prefIsLocked(pref) + ); + + function setInputsDisabledState(isControlled) { + for (let element of gConnectionsDialog.getProxyControls()) { + element.disabled = isControlled; + } + gConnectionsDialog.proxyTypeChanged(); + } + + if (isLocked) { + // An extension can't control this setting if any pref is locked. + hideControllingExtension(PROXY_KEY); + } else { + handleControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).then( + setInputsDisabledState + ); + } + }, + + get dnsOverHttpsResolvers() { + let rawValue = Preferences.get("network.trr.resolvers", "").value; + // if there's no default, we'll hold its position with an empty string + let defaultURI = Preferences.get("network.trr.uri", "").defaultValue; + let providers = []; + if (rawValue) { + try { + providers = JSON.parse(rawValue); + } catch (ex) { + Cu.reportError( + `Bad JSON data in pref network.trr.resolvers: ${rawValue}` + ); + } + } + if (!Array.isArray(providers)) { + Cu.reportError( + `Expected a JSON array in network.trr.resolvers: ${rawValue}` + ); + providers = []; + } + let defaultIndex = providers.findIndex(p => p.url == defaultURI); + if (defaultIndex == -1 && defaultURI) { + // the default value for the pref isn't included in the resolvers list + // so we'll make a stub for it. Without an id, we'll have to use the url as the label + providers.unshift({ url: defaultURI }); + } + return providers; + }, + + isDnsOverHttpsLocked() { + return Services.prefs.prefIsLocked("network.trr.mode"); + }, + + isDnsOverHttpsEnabled() { + // We consider DoH enabled if: + // 1. network.trr.mode has a user-set value equal to 2 or 3. + // 2. network.trr.mode is 0, and DoH heuristics are enabled + let trrPref = Preferences.get("network.trr.mode"); + if (trrPref.value > 0) { + return trrPref.value == 2 || trrPref.value == 3; + } + + let rolloutEnabled = Preferences.get("doh-rollout.enabled").value; + let heuristicsDisabled = + Preferences.get("doh-rollout.disable-heuristics").value || + Preferences.get("doh-rollout.skipHeuristicsCheck").value; + return rolloutEnabled && !heuristicsDisabled; + }, + + readDnsOverHttpsMode() { + // called to update checked element property to reflect current pref value + let enabled = this.isDnsOverHttpsEnabled(); + let uriPref = Preferences.get("network.trr.uri"); + uriPref.updateControlDisabledState(!enabled || this.isDnsOverHttpsLocked()); + // this is the first signal we get when the prefs are available, so + // lazy-init if appropriate + if (!this._areTrrPrefsReady) { + this._areTrrPrefsReady = true; + this._handleTrrPrefsReady(); + } else { + this.updateDnsOverHttpsUI(); + } + return enabled; + }, + + writeDnsOverHttpsMode() { + // called to update pref with user change + let trrModeCheckbox = document.getElementById("networkDnsOverHttps"); + // we treat checked/enabled as mode 2 + return trrModeCheckbox.checked ? 2 : 5; + }, + + updateDnsOverHttpsUI() { + // init and update of the UI must wait until the pref values are ready + if (!this._areTrrPrefsReady) { + return; + } + let [menu, customInput] = this.getDnsOverHttpsControls(); + let customDohContainer = document.getElementById( + "customDnsOverHttpsContainer" + ); + let customURI = Preferences.get("network.trr.custom_uri").value; + let currentURI = Preferences.get("network.trr.uri").value; + let resolvers = this.dnsOverHttpsResolvers; + let isCustom = menu.value == "custom"; + + if (this.isDnsOverHttpsEnabled()) { + this.toggleDnsOverHttpsUI(false); + if (isCustom) { + // if the current and custom_uri values mismatch, update the uri pref + if ( + currentURI && + !customURI && + !resolvers.find(r => r.url == currentURI) + ) { + Services.prefs.setStringPref("network.trr.custom_uri", currentURI); + } + } + } else { + this.toggleDnsOverHttpsUI(true); + } + + if (!menu.disabled && isCustom) { + customDohContainer.hidden = false; + customInput.disabled = false; + customInput.scrollIntoView(); + } else { + customDohContainer.hidden = true; + customInput.disabled = true; + } + + // The height has likely changed, find our SubDialog and tell it to resize. + requestAnimationFrame(() => { + let dialogs = window.opener.gSubDialog._dialogs; + let dialog = dialogs.find(d => d._frame.contentDocument == document); + if (dialog) { + dialog.resizeVertically(); + } + }); + }, + + getDnsOverHttpsControls() { + return [ + document.getElementById("networkDnsOverHttpsResolverChoices"), + document.getElementById("networkCustomDnsOverHttpsInput"), + document.getElementById("networkDnsOverHttpsResolverChoicesLabel"), + document.getElementById("networkCustomDnsOverHttpsInputLabel"), + ]; + }, + + toggleDnsOverHttpsUI(disabled) { + for (let element of this.getDnsOverHttpsControls()) { + element.disabled = disabled; + } + }, + + initDnsOverHttpsUI() { + let resolvers = this.dnsOverHttpsResolvers; + let defaultURI = Preferences.get("network.trr.uri").defaultValue; + let currentURI = Preferences.get("network.trr.uri").value; + let menu = document.getElementById("networkDnsOverHttpsResolverChoices"); + + // populate the DNS-Over-HTTPs resolver list + menu.removeAllItems(); + for (let resolver of resolvers) { + let item = menu.appendItem(undefined, resolver.url); + if (resolver.url == defaultURI) { + document.l10n.setAttributes( + item, + "connection-dns-over-https-url-item-default", + { + name: resolver.name || resolver.url, + } + ); + } else { + item.label = resolver.name || resolver.url; + } + } + let lastItem = menu.appendItem(undefined, "custom"); + document.l10n.setAttributes( + lastItem, + "connection-dns-over-https-url-custom" + ); + + // set initial selection in the resolver provider picker + let selectedIndex = currentURI + ? resolvers.findIndex(r => r.url == currentURI) + : 0; + if (selectedIndex == -1) { + // select the last "Custom" item + selectedIndex = menu.itemCount - 1; + } + menu.selectedIndex = selectedIndex; + + if (this.isDnsOverHttpsLocked()) { + // disable all the options and the checkbox itself to disallow enabling them + this.toggleDnsOverHttpsUI(true); + document.getElementById("networkDnsOverHttps").disabled = true; + } else { + this.toggleDnsOverHttpsUI(false); + this.updateDnsOverHttpsUI(); + document.getElementById("networkDnsOverHttps").disabled = false; + } + }, + + registerSyncPrefListeners() { + function setSyncFromPrefListener(element_id, callback) { + Preferences.addSyncFromPrefListener( + document.getElementById(element_id), + callback + ); + } + function setSyncToPrefListener(element_id, callback) { + Preferences.addSyncToPrefListener( + document.getElementById(element_id), + callback + ); + } + setSyncFromPrefListener("networkProxyType", () => this.readProxyType()); + setSyncFromPrefListener("networkProxyHTTP", () => + this.readHTTPProxyServer() + ); + setSyncFromPrefListener("networkProxyHTTP_Port", () => + this.readHTTPProxyPort() + ); + setSyncFromPrefListener("shareAllProxies", () => + this.updateProtocolPrefs() + ); + setSyncFromPrefListener("networkProxySSL", () => + this.readProxyProtocolPref("ssl", false) + ); + setSyncFromPrefListener("networkProxySSL_Port", () => + this.readProxyProtocolPref("ssl", true) + ); + setSyncFromPrefListener("networkProxyFTP", () => + this.readProxyProtocolPref("ftp", false) + ); + setSyncFromPrefListener("networkProxyFTP_Port", () => + this.readProxyProtocolPref("ftp", true) + ); + setSyncFromPrefListener("networkProxySOCKS", () => + this.readProxyProtocolPref("socks", false) + ); + setSyncFromPrefListener("networkProxySOCKS_Port", () => + this.readProxyProtocolPref("socks", true) + ); + setSyncFromPrefListener("networkDnsOverHttps", () => + this.readDnsOverHttpsMode() + ); + setSyncToPrefListener("networkDnsOverHttps", () => + this.writeDnsOverHttpsMode() + ); + }, +}; diff --git a/browser/components/preferences/dialogs/connection.xhtml b/browser/components/preferences/dialogs/connection.xhtml new file mode 100644 index 0000000000..4508e7c022 --- /dev/null +++ b/browser/components/preferences/dialogs/connection.xhtml @@ -0,0 +1,170 @@ +<?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-window" + data-l10n-attrs="title, style" + persist="lastSelected" + onload="gConnectionsDialog.checkForSystemProxy();"> +<dialog id="ConnectionsDialog" + buttons="accept,cancel,help" + helpTopic="prefs-connection-settings"> + + <!-- Used for extension-controlled lockdown message --> + <linkset> + <html:link rel="localization" href="browser/preferences/connection.ftl"/> + <html:link rel="localization" href="browser/preferences/preferences.ftl"/> + <html:link rel="localization" href="branding/brand.ftl"/> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js"/> + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://browser/content/preferences/extensionControlled.js"/> + + <keyset> + <key data-l10n-id="connection-close-key" modifiers="accel" oncommand="Preferences.close(event)"/> + </keyset> + + <script src="chrome://browser/content/preferences/dialogs/connection.js"/> + + <hbox id="proxyExtensionContent" + align="start" hidden="true" class="extension-controlled"> + <description control="disableProxyExtension" flex="1" /> + <button id="disableProxyExtension" + class="extension-controlled-button accessory-button" + data-l10n-id="connection-disable-extension" /> + </hbox> + + <groupbox> + <label><html:h2 data-l10n-id="connection-proxy-configure"/></label> + + <radiogroup id="networkProxyType" preference="network.proxy.type"> + <radio value="0" data-l10n-id="connection-proxy-option-no" /> + <radio value="4" data-l10n-id="connection-proxy-option-auto" /> + <radio value="5" data-l10n-id="connection-proxy-option-system" id="systemPref" hidden="true" /> + <radio value="1" data-l10n-id="connection-proxy-option-manual"/> + <box id="proxy-grid" class="indent" flex="1"> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label data-l10n-id="connection-proxy-http" control="networkProxyHTTP" /> + </hbox> + <hbox align="center"> + <html:input id="networkProxyHTTP" type="text" style="-moz-box-flex: 1;" + preference="network.proxy.http"/> + <label data-l10n-id="connection-proxy-http-port" control="networkProxyHTTP_Port" /> + <html:input id="networkProxyHTTP_Port" class="proxy-port-input" hidespinbuttons="true" type="number" min="0" max="65535" + preference="network.proxy.http_port"/> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <hbox/> + <hbox> + <checkbox id="shareAllProxies" data-l10n-id="connection-proxy-http-sharing" + preference="network.proxy.share_proxy_settings"/> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label data-l10n-id="connection-proxy-https" control="networkProxySSL"/> + </hbox> + <hbox align="center"> + <html:input id="networkProxySSL" type="text" style="-moz-box-flex: 1;" preference="network.proxy.ssl"/> + <label data-l10n-id="connection-proxy-ssl-port" control="networkProxySSL_Port" /> + <html:input id="networkProxySSL_Port" class="proxy-port-input" hidespinbuttons="true" type="number" min="0" max="65535" size="5" + preference="network.proxy.ssl_port"/> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label data-l10n-id="connection-proxy-ftp" control="networkProxyFTP"/> + </hbox> + <hbox align="center"> + <html:input id="networkProxyFTP" type="text" style="-moz-box-flex: 1;" preference="network.proxy.ftp"/> + <label data-l10n-id="connection-proxy-ftp-port" control="networkProxyFTP_Port"/> + <html:input id="networkProxyFTP_Port" class="proxy-port-input" hidespinbuttons="true" type="number" min="0" max="65535" size="5" + preference="network.proxy.ftp_port"/> + </hbox> + </html:div> + <separator class="thin"/> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label data-l10n-id="connection-proxy-socks" control="networkProxySOCKS"/> + </hbox> + <hbox align="center"> + <html:input id="networkProxySOCKS" type="text" style="-moz-box-flex: 1;" preference="network.proxy.socks"/> + <label data-l10n-id="connection-proxy-socks-port" control="networkProxySOCKS_Port"/> + <html:input id="networkProxySOCKS_Port" class="proxy-port-input" hidespinbuttons="true" type="number" min="0" max="65535" size="5" + preference="network.proxy.socks_port"/> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <spacer/> + <box pack="start"> + <radiogroup id="networkProxySOCKSVersion" orient="horizontal" + preference="network.proxy.socks_version"> + <radio id="networkProxySOCKSVersion4" value="4" data-l10n-id="connection-proxy-socks4" /> + <radio id="networkProxySOCKSVersion5" value="5" data-l10n-id="connection-proxy-socks5" /> + </radiogroup> + </box> + </html:div> + </box> + <radio value="2" data-l10n-id="connection-proxy-autotype" /> + <hbox class="indent" flex="1" align="center"> + <html:input id="networkProxyAutoconfigURL" type="text" style="-moz-box-flex: 1;" preference="network.proxy.autoconfig_url" + oninput="gConnectionsDialog.updateReloadButton();"/> + <button id="autoReload" + data-l10n-id="connection-proxy-reload" + oncommand="gConnectionsDialog.reloadPAC();" + preference="pref.advanced.proxies.disable_button.reload"/> + </hbox> + </radiogroup> + </groupbox> + <separator class="thin"/> + <label data-l10n-id="connection-proxy-noproxy" control="networkProxyNone"/> + <html:textarea id="networkProxyNone" preference="network.proxy.no_proxies_on" rows="2"/> + <label control="networkProxyNone" data-l10n-id="connection-proxy-noproxy-desc" /> + <label id="networkProxyNoneLocalhost" control="networkProxyNone" data-l10n-id="connection-proxy-noproxy-localhost-desc-2" /> + <separator class="thin"/> + <checkbox id="autologinProxy" + data-l10n-id="connection-proxy-autologin" + preference="signon.autologin.proxy" /> + <checkbox id="networkProxySOCKSRemoteDNS" + preference="network.proxy.socks_remote_dns" + data-l10n-id="connection-proxy-socks-remote-dns" /> + + <groupbox> + <checkbox id="networkDnsOverHttps" + data-l10n-id="connection-dns-over-https" + preference="network.trr.mode"/> + + <box id="dnsOverHttps-grid" class="indent" flex="1"> + <html:div class="dnsOverHttps-grid-row"> + <hbox pack="end"> + <label id="networkDnsOverHttpsResolverChoicesLabel" + data-l10n-id="connection-dns-over-https-url-resolver" + control="networkDnsOverHttpsResolverChoices"/> + </hbox> + <menulist id="networkDnsOverHttpsResolverChoices" + oncommand="gConnectionsDialog.updateDnsOverHttpsUI()"></menulist> + </html:div> + <html:div class="dnsOverHttps-grid-row" id="customDnsOverHttpsContainer" hidden="hidden"> + <hbox pack="end"> + <label id="networkCustomDnsOverHttpsInputLabel" + data-l10n-id="connection-dns-over-https-custom-label" + control="networkCustomDnsOverHttpsInput"/> + </hbox> + <html:input id="networkCustomDnsOverHttpsInput" type="text" style="-moz-box-flex: 1;" + preference="network.trr.custom_uri"/> + </html:div> + </box> + </groupbox> +</dialog> +</window> diff --git a/browser/components/preferences/dialogs/containers.js b/browser/components/preferences/dialogs/containers.js new file mode 100644 index 0000000000..1f88853afc --- /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.import( + "resource://gre/modules/ContextualIdentityService.jsm" +); + +/** + * 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", { + name: params.identity.name, + }); + } else { + document.l10n.setAttributes(winElem, "containers-window-new"); + } +} +setTitle(); + +let gContainersManager = { + icons: [ + "fingerprint", + "briefcase", + "dollar", + "cart", + "vacation", + "gift", + "food", + "fruit", + "pet", + "tree", + "chill", + "circle", + "fence", + ], + + colors: [ + "blue", + "turquoise", + "green", + "yellow", + "orange", + "red", + "pink", + "purple", + "toolbar", + ], + + onLoad() { + let params = window.arguments[0] || {}; + this.init(params); + }, + + init(aParams) { + this._dialog = document.querySelector("dialog"); + this.userContextId = aParams.userContextId || null; + this.identity = aParams.identity; + + const iconWrapper = document.getElementById("iconWrapper"); + iconWrapper.appendChild(this.createIconButtons()); + + const colorWrapper = document.getElementById("colorWrapper"); + colorWrapper.appendChild(this.createColorSwatches()); + + if (this.identity.name) { + const name = document.getElementById("name"); + name.value = this.identity.name; + this.checkForm(); + } + + document.addEventListener("dialogaccept", () => this.onApplyChanges()); + + // This is to prevent layout jank caused by the svgs and outlines rendering at different times + document.getElementById("containers-content").removeAttribute("hidden"); + }, + + uninit() {}, + + // Check if name is provided to determine if the form can be submitted + checkForm() { + const name = document.getElementById("name"); + this._dialog.setAttribute("buttondisabledaccept", !name.value); + }, + + createIconButtons(defaultIcon) { + let radiogroup = document.createXULElement("radiogroup"); + radiogroup.setAttribute("id", "icon"); + radiogroup.className = "icon-buttons radio-buttons"; + + for (let icon of this.icons) { + let iconSwatch = document.createXULElement("radio"); + iconSwatch.id = "iconbutton-" + icon; + iconSwatch.name = "icon"; + iconSwatch.type = "radio"; + iconSwatch.value = icon; + + if (this.identity.icon && this.identity.icon == icon) { + iconSwatch.setAttribute("selected", true); + } + + document.l10n.setAttributes(iconSwatch, `containers-icon-${icon}`); + let iconElement = document.createXULElement("hbox"); + iconElement.className = "userContext-icon"; + iconElement.classList.add("identity-icon-" + icon); + + iconSwatch.appendChild(iconElement); + radiogroup.appendChild(iconSwatch); + } + + return radiogroup; + }, + + createColorSwatches(defaultColor) { + let radiogroup = document.createXULElement("radiogroup"); + radiogroup.setAttribute("id", "color"); + radiogroup.className = "radio-buttons"; + + for (let color of this.colors) { + let colorSwatch = document.createXULElement("radio"); + colorSwatch.id = "colorswatch-" + color; + colorSwatch.name = "color"; + colorSwatch.type = "radio"; + colorSwatch.value = color; + + if (this.identity.color && this.identity.color == color) { + colorSwatch.setAttribute("selected", true); + } + + document.l10n.setAttributes(colorSwatch, `containers-color-${color}`); + let iconElement = document.createXULElement("hbox"); + iconElement.className = "userContext-icon"; + iconElement.classList.add("identity-icon-circle"); + iconElement.classList.add("identity-color-" + color); + + colorSwatch.appendChild(iconElement); + radiogroup.appendChild(colorSwatch); + } + return radiogroup; + }, + + onApplyChanges() { + let icon = document.getElementById("icon").value; + let color = document.getElementById("color").value; + let name = document.getElementById("name").value; + + if (!this.icons.includes(icon)) { + throw new Error("Internal error. The icon value doesn't match."); + } + + if (!this.colors.includes(color)) { + throw new Error("Internal error. The color value doesn't match."); + } + + if (this.userContextId) { + ContextualIdentityService.update(this.userContextId, name, icon, color); + } else { + ContextualIdentityService.create(name, icon, color); + } + window.parent.location.reload(); + }, +}; diff --git a/browser/components/preferences/dialogs/containers.xhtml b/browser/components/preferences/dialogs/containers.xhtml new file mode 100644 index 0000000000..a933f31d8a --- /dev/null +++ b/browser/components/preferences/dialogs/containers.xhtml @@ -0,0 +1,53 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/containers-dialog.css" type="text/css"?> + +<window id="ContainersDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-attrs="title, style" + onload="gContainersManager.onLoad();" + onunload="gContainersManager.uninit();" + persist="width height"> + + <dialog + buttons="accept" + buttondisabledaccept="true" + data-l10n-id="containers-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"> + + <linkset> + <html:link rel="localization" href="browser/preferences/containers.ftl"/> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/containers.js"/> + + <keyset> + <key data-l10n-id="containers-window-close" modifiers="accel" oncommand="window.close();"/> + </keyset> + + <vbox class="contentPane" hidden="true" id="containers-content"> + <hbox align="start"> + <label id="nameLabel" control="name" + data-l10n-id="containers-name-label" + data-l10n-attrs="style"/> + <html:input id="name" type="text" data-l10n-id="containers-name-text" oninput="gContainersManager.checkForm();" /> + </hbox> + <hbox align="center" id="colorWrapper"> + <label id="colorLabel" control="color" + data-l10n-id="containers-color-label" + data-l10n-attrs="style"/> + </hbox> + <hbox align="center" id="iconWrapper"> + <label id="iconLabel" control="icon" + data-l10n-id="containers-icon-label" + data-l10n-attrs="style"/> + </hbox> + </vbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/fonts.js b/browser/components/preferences/dialogs/fonts.js new file mode 100644 index 0000000000..e112d43215 --- /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 ../../../base/content/utilityOverlay.js */ +/* import-globals-from ../../../../toolkit/mozapps/preferences/fontbuilder.js */ + +// browser.display.languageList LOCK ALL when LOCKED + +const kDefaultFontType = "font.default.%LANG%"; +const kFontNameFmtSerif = "font.name.serif.%LANG%"; +const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%"; +const kFontNameFmtMonospace = "font.name.monospace.%LANG%"; +const kFontNameListFmtSerif = "font.name-list.serif.%LANG%"; +const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%"; +const kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%"; +const kFontSizeFmtVariable = "font.size.variable.%LANG%"; +const kFontSizeFmtFixed = "font.size.monospace.%LANG%"; +const kFontMinSizeFmt = "font.minimum-size.%LANG%"; + +document + .getElementById("FontsDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); +window.addEventListener("load", () => gFontsDialog.onLoad()); + +Preferences.addAll([ + { id: "font.language.group", type: "wstring" }, + { id: "browser.display.use_document_fonts", type: "int" }, +]); + +var gFontsDialog = { + _selectLanguageGroupPromise: Promise.resolve(), + + onLoad() { + Preferences.addSyncFromPrefListener( + document.getElementById("selectLangs"), + () => this.readFontLanguageGroup() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("useDocumentFonts"), + () => this.readUseDocumentFonts() + ); + Preferences.addSyncToPrefListener( + document.getElementById("useDocumentFonts"), + () => this.writeUseDocumentFonts() + ); + for (let id of ["serif", "sans-serif", "monospace"]) { + let el = document.getElementById(id); + Preferences.addSyncFromPrefListener(el, () => + FontBuilder.readFontSelection(el) + ); + } + }, + + _selectLanguageGroup(aLanguageGroup) { + this._selectLanguageGroupPromise = (async () => { + // Avoid overlapping language group selections by awaiting the resolution + // of the previous one. We do this because this function is re-entrant, + // as inserting <preference> elements into the DOM sometimes triggers a call + // back into this function. And since this function is also asynchronous, + // that call can enter this function before the previous run has completed, + // which would corrupt the font menulists. Awaiting the previous call's + // resolution avoids that fate. + await this._selectLanguageGroupPromise; + + var prefs = [ + { + format: kDefaultFontType, + type: "string", + element: "defaultFontType", + fonttype: null, + }, + { + format: kFontNameFmtSerif, + type: "fontname", + element: "serif", + fonttype: "serif", + }, + { + format: kFontNameFmtSansSerif, + type: "fontname", + element: "sans-serif", + fonttype: "sans-serif", + }, + { + format: kFontNameFmtMonospace, + type: "fontname", + element: "monospace", + fonttype: "monospace", + }, + { + format: kFontNameListFmtSerif, + type: "unichar", + element: null, + fonttype: "serif", + }, + { + format: kFontNameListFmtSansSerif, + type: "unichar", + element: null, + fonttype: "sans-serif", + }, + { + format: kFontNameListFmtMonospace, + type: "unichar", + element: null, + fonttype: "monospace", + }, + { + format: kFontSizeFmtVariable, + type: "int", + element: "sizeVar", + fonttype: null, + }, + { + format: kFontSizeFmtFixed, + type: "int", + element: "sizeMono", + fonttype: null, + }, + { + format: kFontMinSizeFmt, + type: "int", + element: "minSize", + fonttype: null, + }, + ]; + for (var i = 0; i < prefs.length; ++i) { + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + var preference = Preferences.get(name); + if (!preference) { + preference = Preferences.add({ id: name, type: prefs[i].type }); + } + + if (!prefs[i].element) { + continue; + } + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) { + await FontBuilder.buildFontList( + aLanguageGroup, + prefs[i].fonttype, + element + ); + } + + preference.setElementValue(element); + } + } + })().catch(Cu.reportError); + }, + + readFontLanguageGroup() { + var languagePref = Preferences.get("font.language.group"); + this._selectLanguageGroup(languagePref.value); + return undefined; + }, + + readUseDocumentFonts() { + var preference = Preferences.get("browser.display.use_document_fonts"); + return preference.value == 1; + }, + + writeUseDocumentFonts() { + var useDocumentFonts = document.getElementById("useDocumentFonts"); + return useDocumentFonts.checked ? 1 : 0; + }, +}; diff --git a/browser/components/preferences/dialogs/fonts.xhtml b/browser/components/preferences/dialogs/fonts.xhtml new file mode 100644 index 0000000000..7bfe065df8 --- /dev/null +++ b/browser/components/preferences/dialogs/fonts.xhtml @@ -0,0 +1,240 @@ +<?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> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <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> + </hbox> + + <separator class="thin"/> + + <box style="display: grid; grid-template-columns: auto 1fr auto auto;"> + <!-- proportional row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-proportional-header" control="defaultFontType"/> + </hbox> + <!-- This <hbox> is needed to position search tooltips correctly. --> + <hbox> + <menulist id="defaultFontType" flex="1" style="width: 0px;"> + <menupopup> + <menuitem value="serif" data-l10n-id="fonts-default-serif"/> + <menuitem value="sans-serif" data-l10n-id="fonts-default-sans-serif"/> + </menupopup> + </menulist> + </hbox> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-proportional-size" control="sizeVar"/> + </hbox> + <!-- This <hbox> is needed to position search tooltips correctly. --> + <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> + </hbox> + + <!-- serif row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-serif" control="serif"/> + </hbox> + <hbox> + <menulist id="serif" flex="1" style="width: 0px;" delayprefsave="true"/> + </hbox> + <spacer/> + <spacer/> + + <!-- sans-serif row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-sans-serif" control="sans-serif"/> + </hbox> + <hbox> + <menulist id="sans-serif" flex="1" style="width: 0px;" delayprefsave="true"/> + </hbox> + <spacer/> + <spacer/> + + <!-- monospace row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-monospace" control="monospace"/> + </hbox> + <hbox> + <menulist id="monospace" flex="1" style="width: 0px;" crop="right" delayprefsave="true"/> + </hbox> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-monospace-size" control="sizeMono"/> + </hbox> + <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> + </hbox> + </box> + <separator class="thin"/> + <hbox pack="end"> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-minsize" control="minSize"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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> + </hbox> + </hbox> + <separator/> + <separator class="groove"/> + <hbox> + <checkbox id="useDocumentFonts" + data-l10n-id="fonts-allow-own" + preference="browser.display.use_document_fonts"/> + </hbox> + </groupbox> + + <!-- Load the script after the elements for layout issues (bug 1501755). --> + <script src="chrome://mozapps/content/preferences/fontbuilder.js"/> + <script src="chrome://browser/content/preferences/dialogs/fonts.js"/> +</dialog> +</window> diff --git a/browser/components/preferences/dialogs/handlers.css b/browser/components/preferences/dialogs/handlers.css new file mode 100644 index 0000000000..8f7becd81d --- /dev/null +++ b/browser/components/preferences/dialogs/handlers.css @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Make the icons appear. + * Note: we display the icon box for every item whether or not it has an icon + * so the labels of all the items align vertically. + */ +.actionsMenu > menupopup > menuitem > .menu-iconic-left { + display: -moz-box; + min-width: 16px; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + #handlersView > richlistitem, + .actionsMenu > menupopup > menuitem > .menu-iconic-left { + image-rendering: -moz-crisp-edges; + } +} diff --git a/browser/components/preferences/dialogs/jar.mn b/browser/components/preferences/dialogs/jar.mn new file mode 100644 index 0000000000..05050dc37b --- /dev/null +++ b/browser/components/preferences/dialogs/jar.mn @@ -0,0 +1,45 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/preferences/dialogs/addEngine.xhtml + content/browser/preferences/dialogs/addEngine.js + content/browser/preferences/dialogs/addEngine.css + content/browser/preferences/dialogs/applicationManager.xhtml + content/browser/preferences/dialogs/applicationManager.js + content/browser/preferences/dialogs/blocklists.xhtml + content/browser/preferences/dialogs/blocklists.js + content/browser/preferences/dialogs/browserLanguages.xhtml + content/browser/preferences/dialogs/browserLanguages.js + content/browser/preferences/dialogs/clearSiteData.css + content/browser/preferences/dialogs/clearSiteData.js + content/browser/preferences/dialogs/clearSiteData.xhtml + content/browser/preferences/dialogs/colors.xhtml + content/browser/preferences/dialogs/colors.js + content/browser/preferences/dialogs/connection.xhtml + content/browser/preferences/dialogs/connection.js + content/browser/preferences/dialogs/fonts.xhtml + content/browser/preferences/dialogs/fonts.js + content/browser/preferences/dialogs/handlers.css + content/browser/preferences/dialogs/languages.xhtml + content/browser/preferences/dialogs/languages.js + content/browser/preferences/dialogs/permissions.xhtml + content/browser/preferences/dialogs/sitePermissions.xhtml + content/browser/preferences/dialogs/sitePermissions.js + content/browser/preferences/dialogs/sitePermissions.css + content/browser/preferences/dialogs/containers.xhtml + content/browser/preferences/dialogs/containers.js + content/browser/preferences/dialogs/permissions.js + content/browser/preferences/dialogs/sanitize.xhtml + content/browser/preferences/dialogs/sanitize.js + content/browser/preferences/dialogs/selectBookmark.xhtml + content/browser/preferences/dialogs/selectBookmark.js + content/browser/preferences/dialogs/siteDataSettings.xhtml + content/browser/preferences/dialogs/siteDataSettings.js +* content/browser/preferences/dialogs/siteDataRemoveSelected.xhtml + content/browser/preferences/dialogs/siteDataRemoveSelected.js + content/browser/preferences/dialogs/syncChooseWhatToSync.xhtml + content/browser/preferences/dialogs/syncChooseWhatToSync.js + content/browser/preferences/dialogs/translation.xhtml + content/browser/preferences/dialogs/translation.js diff --git a/browser/components/preferences/dialogs/languages.js b/browser/components/preferences/dialogs/languages.js new file mode 100644 index 0000000000..8086900671 --- /dev/null +++ b/browser/components/preferences/dialogs/languages.js @@ -0,0 +1,379 @@ +/* -*- 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 */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +document + .getElementById("LanguagesDialog") + .addEventListener("dialoghelp", window.top.openPrefsHelp); + +Preferences.addAll([ + { id: "intl.accept_languages", type: "wstring" }, + { id: "pref.browser.language.disable_button.up", type: "bool" }, + { id: "pref.browser.language.disable_button.down", type: "bool" }, + { id: "pref.browser.language.disable_button.remove", type: "bool" }, + { id: "privacy.spoof_english", type: "int" }, +]); + +var gLanguagesDialog = { + _availableLanguagesList: [], + _acceptLanguages: {}, + + _selectedItemID: null, + + onLoad() { + let spoofEnglishElement = document.getElementById("spoofEnglish"); + Preferences.addSyncFromPrefListener(spoofEnglishElement, () => + gLanguagesDialog.readSpoofEnglish() + ); + Preferences.addSyncToPrefListener(spoofEnglishElement, () => + gLanguagesDialog.writeSpoofEnglish() + ); + + Preferences.get("intl.accept_languages").on("change", () => + this._readAcceptLanguages().catch(Cu.reportError) + ); + + if (!this._availableLanguagesList.length) { + document.mozSubdialogReady = this._loadAvailableLanguages(); + } + }, + + get _activeLanguages() { + return document.getElementById("activeLanguages"); + }, + + get _availableLanguages() { + return document.getElementById("availableLanguages"); + }, + + async _loadAvailableLanguages() { + // This is a parser for: resource://gre/res/language.properties + // The file is formatted like so: + // ab[-cd].accept=true|false + // ab = language + // cd = region + var bundleAccepted = document.getElementById("bundleAccepted"); + + function LocaleInfo(aLocaleName, aLocaleCode, aIsVisible) { + this.name = aLocaleName; + this.code = aLocaleCode; + this.isVisible = aIsVisible; + } + + // 1) Read the available languages out of language.properties + + let localeCodes = []; + let localeValues = []; + for (let currString of bundleAccepted.strings) { + var property = currString.key.split("."); // ab[-cd].accept + if (property[1] == "accept") { + localeCodes.push(property[0]); + localeValues.push(currString.value); + } + } + + let localeNames = Services.intl.getLocaleDisplayNames( + undefined, + localeCodes + ); + + for (let i in localeCodes) { + let isVisible = + localeValues[i] == "true" && + (!(localeCodes[i] in this._acceptLanguages) || + !this._acceptLanguages[localeCodes[i]]); + + let li = new LocaleInfo(localeNames[i], localeCodes[i], isVisible); + this._availableLanguagesList.push(li); + } + + await this._buildAvailableLanguageList(); + await this._readAcceptLanguages(); + }, + + async _buildAvailableLanguageList() { + var availableLanguagesPopup = document.getElementById( + "availableLanguagesPopup" + ); + while (availableLanguagesPopup.hasChildNodes()) { + availableLanguagesPopup.firstChild.remove(); + } + + let frag = document.createDocumentFragment(); + + // Load the UI with the data + for (var i = 0; i < this._availableLanguagesList.length; ++i) { + let locale = this._availableLanguagesList[i]; + let localeCode = locale.code; + if ( + locale.isVisible && + (!(localeCode in this._acceptLanguages) || + !this._acceptLanguages[localeCode]) + ) { + var menuitem = document.createXULElement("menuitem"); + menuitem.id = localeCode; + document.l10n.setAttributes(menuitem, "languages-code-format", { + locale: locale.name, + code: localeCode, + }); + frag.appendChild(menuitem); + } + } + + await document.l10n.translateFragment(frag); + + // Sort the list of languages by name + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + let items = Array.from(frag.children); + + items.sort((a, b) => { + return comp.compare(a.getAttribute("label"), b.getAttribute("label")); + }); + + // Re-append items in the correct order: + items.forEach(item => frag.appendChild(item)); + + availableLanguagesPopup.appendChild(frag); + + this._availableLanguages.setAttribute( + "label", + this._availableLanguages.getAttribute("placeholder") + ); + }, + + async _readAcceptLanguages() { + while (this._activeLanguages.hasChildNodes()) { + this._activeLanguages.firstChild.remove(); + } + + var selectedIndex = 0; + var preference = Preferences.get("intl.accept_languages"); + if (preference.value == "") { + return; + } + var languages = preference.value.toLowerCase().split(/\s*,\s*/); + for (var i = 0; i < languages.length; ++i) { + var listitem = document.createXULElement("richlistitem"); + var label = document.createXULElement("label"); + listitem.appendChild(label); + listitem.id = languages[i]; + if (languages[i] == this._selectedItemID) { + selectedIndex = i; + } + this._activeLanguages.appendChild(listitem); + var localeName = this._getLocaleName(languages[i]); + document.l10n.setAttributes(label, "languages-active-code-format", { + locale: localeName, + code: languages[i], + }); + + // Hash this language as an "Active" language so we don't + // show it in the list that can be added. + this._acceptLanguages[languages[i]] = true; + } + + // We're forcing an early localization here because otherwise + // the initial sizing of the dialog will happen before it and + // result in overflow. + await document.l10n.translateFragment(this._activeLanguages); + + if (this._activeLanguages.childNodes.length) { + this._activeLanguages.ensureIndexIsVisible(selectedIndex); + this._activeLanguages.selectedIndex = selectedIndex; + } + + // Update states of accept-language list and buttons according to + // privacy.resistFingerprinting and privacy.spoof_english. + this.readSpoofEnglish(); + }, + + onAvailableLanguageSelect() { + var availableLanguages = this._availableLanguages; + var addButton = document.getElementById("addButton"); + addButton.disabled = + availableLanguages.disabled || availableLanguages.selectedIndex < 0; + + this._availableLanguages.removeAttribute("accesskey"); + }, + + addLanguage() { + var selectedID = this._availableLanguages.selectedItem.id; + var preference = Preferences.get("intl.accept_languages"); + var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/); + for (var i = 0; i < arrayOfPrefs.length; ++i) { + if (arrayOfPrefs[i] == selectedID) { + return; + } + } + + this._selectedItemID = selectedID; + + if (preference.value == "") { + preference.value = selectedID; + } else { + arrayOfPrefs.unshift(selectedID); + preference.value = arrayOfPrefs.join(","); + } + + this._acceptLanguages[selectedID] = true; + this._availableLanguages.selectedItem = null; + + // Rebuild the available list with the added item removed... + this._buildAvailableLanguageList().catch(Cu.reportError); + }, + + removeLanguage() { + // Build the new preference value string. + var languagesArray = []; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + if (!item.selected) { + languagesArray.push(item.id); + } else { + this._acceptLanguages[item.id] = false; + } + } + var string = languagesArray.join(","); + + // Get the item to select after the remove operation completes. + var selection = this._activeLanguages.selectedItems; + var lastSelected = selection[selection.length - 1]; + var selectItem = lastSelected.nextSibling || lastSelected.previousSibling; + selectItem = selectItem ? selectItem.id : null; + + this._selectedItemID = selectItem; + + // Update the preference and force a UI rebuild + var preference = Preferences.get("intl.accept_languages"); + preference.value = string; + + this._buildAvailableLanguageList().catch(Cu.reportError); + }, + + _getLocaleName(localeCode) { + if (!this._availableLanguagesList.length) { + this._loadAvailableLanguages(); + } + for (var i = 0; i < this._availableLanguagesList.length; ++i) { + if (localeCode == this._availableLanguagesList[i].code) { + return this._availableLanguagesList[i].name; + } + } + return ""; + }, + + moveUp() { + var selectedItem = this._activeLanguages.selectedItems[0]; + var previousItem = selectedItem.previousSibling; + + var string = ""; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + string += i == 0 ? "" : ","; + if (item.id == previousItem.id) { + string += selectedItem.id; + } else if (item.id == selectedItem.id) { + string += previousItem.id; + } else { + string += item.id; + } + } + + this._selectedItemID = selectedItem.id; + + // Update the preference and force a UI rebuild + var preference = Preferences.get("intl.accept_languages"); + preference.value = string; + }, + + moveDown() { + var selectedItem = this._activeLanguages.selectedItems[0]; + var nextItem = selectedItem.nextSibling; + + var string = ""; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + string += i == 0 ? "" : ","; + if (item.id == nextItem.id) { + string += selectedItem.id; + } else if (item.id == selectedItem.id) { + string += nextItem.id; + } else { + string += item.id; + } + } + + this._selectedItemID = selectedItem.id; + + // Update the preference and force a UI rebuild + var preference = Preferences.get("intl.accept_languages"); + preference.value = string; + }, + + onLanguageSelect() { + var upButton = document.getElementById("up"); + var downButton = document.getElementById("down"); + var removeButton = document.getElementById("remove"); + switch (this._activeLanguages.selectedCount) { + case 0: + upButton.disabled = downButton.disabled = removeButton.disabled = true; + break; + case 1: + upButton.disabled = this._activeLanguages.selectedIndex == 0; + downButton.disabled = + this._activeLanguages.selectedIndex == + this._activeLanguages.childNodes.length - 1; + removeButton.disabled = false; + break; + default: + upButton.disabled = true; + downButton.disabled = true; + removeButton.disabled = false; + } + }, + + readSpoofEnglish() { + var checkbox = document.getElementById("spoofEnglish"); + var resistFingerprinting = Services.prefs.getBoolPref( + "privacy.resistFingerprinting" + ); + if (!resistFingerprinting) { + checkbox.hidden = true; + return false; + } + + var spoofEnglish = Preferences.get("privacy.spoof_english").value; + var activeLanguages = this._activeLanguages; + var availableLanguages = this._availableLanguages; + checkbox.hidden = false; + switch (spoofEnglish) { + case 1: // don't spoof intl.accept_languages + activeLanguages.disabled = false; + activeLanguages.selectItem(activeLanguages.firstChild); + availableLanguages.disabled = false; + this.onAvailableLanguageSelect(); + return false; + case 2: // spoof intl.accept_languages + activeLanguages.clearSelection(); + activeLanguages.disabled = true; + availableLanguages.disabled = true; + this.onAvailableLanguageSelect(); + return true; + default: + // will prompt for spoofing intl.accept_languages if resisting fingerprinting + return false; + } + }, + + writeSpoofEnglish() { + return document.getElementById("spoofEnglish").checked ? 2 : 1; + }, +}; diff --git a/browser/components/preferences/dialogs/languages.xhtml b/browser/components/preferences/dialogs/languages.xhtml new file mode 100644 index 0000000000..24b79c3eda --- /dev/null +++ b/browser/components/preferences/dialogs/languages.xhtml @@ -0,0 +1,67 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<window type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="webpage-languages-window" + data-l10n-attrs="title, style" + persist="lastSelected" + onload="gLanguagesDialog.onLoad();"> +<dialog id="LanguagesDialog" + buttons="accept,cancel,help" + helpTopic="prefs-languages"> + + <linkset> + <html:link rel="localization" href="browser/preferences/languages.ftl"/> + </linkset> + + <script src="chrome://browser/content/utilityOverlay.js"/> + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://browser/content/preferences/dialogs/languages.js"/> + + <keyset> + <key data-l10n-id="languages-close-key" modifiers="accel" oncommand="Preferences.close(event)"/> + </keyset> + + <stringbundleset id="languageSet"> + <stringbundle id="bundleAccepted" src="resource://gre/res/language.properties"/> + </stringbundleset> + + <description data-l10n-id="languages-description"/> + <checkbox id="spoofEnglish" + data-l10n-id="languages-customize-spoof-english" + preference="privacy.spoof_english"/> + <box flex="1" style="display: grid; grid-template-rows: 1fr auto; grid-template-columns: 1fr auto;"> + <richlistbox id="activeLanguages" + seltype="multiple" + onselect="gLanguagesDialog.onLanguageSelect();"/> + <vbox> + <button id="up" class="up" oncommand="gLanguagesDialog.moveUp();" disabled="true" + data-l10n-id="languages-customize-moveup" + preference="pref.browser.language.disable_button.up"/> + <button id="down" class="down" oncommand="gLanguagesDialog.moveDown();" disabled="true" + data-l10n-id="languages-customize-movedown" + preference="pref.browser.language.disable_button.down"/> + <button id="remove" oncommand="gLanguagesDialog.removeLanguage();" disabled="true" + data-l10n-id="languages-customize-remove" + preference="pref.browser.language.disable_button.remove"/> + </vbox> + <!-- This <vbox> is needed to position search tooltips correctly. --> + <vbox> + <menulist id="availableLanguages" oncommand="gLanguagesDialog.onAvailableLanguageSelect();" + data-l10n-id="languages-customize-select-language" data-l10n-attrs="placeholder"> + <menupopup id="availableLanguagesPopup"/> + </menulist> + </vbox> + <button id="addButton" class="add-web-language" oncommand="gLanguagesDialog.addLanguage();" disabled="true" + data-l10n-id="languages-customize-add"/> + </box> +</dialog> +</window> diff --git a/browser/components/preferences/dialogs/moz.build b/browser/components/preferences/dialogs/moz.build new file mode 100644 index 0000000000..603c560505 --- /dev/null +++ b/browser/components/preferences/dialogs/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +for var in ("MOZ_APP_NAME", "MOZ_MACBUNDLE_NAME"): + DEFINES[var] = CONFIG[var] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"): + DEFINES["HAVE_SHELL_SERVICE"] = 1 + +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/components/preferences/dialogs/permissions.js b/browser/components/preferences/dialogs/permissions.js new file mode 100644 index 0000000000..e8a3954678 --- /dev/null +++ b/browser/components/preferences/dialogs/permissions.js @@ -0,0 +1,525 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +const permissionExceptionsL10n = { + trackingprotection: { + window: "permissions-exceptions-etp-window", + description: "permissions-exceptions-etp-desc", + }, + cookie: { + window: "permissions-exceptions-cookie-window", + description: "permissions-exceptions-cookie-desc", + }, + popup: { + window: "permissions-exceptions-popup-window", + description: "permissions-exceptions-popup-desc", + }, + "login-saving": { + window: "permissions-exceptions-saved-logins-window", + description: "permissions-exceptions-saved-logins-desc", + }, + install: { + window: "permissions-exceptions-addons-window", + 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._btnSession = document.getElementById("btnSession"); + this._btnBlock = document.getElementById("btnBlock"); + this._btnAllow = document.getElementById("btnAllow"); + + 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; + + this._urlField = document.getElementById("url"); + this._urlField.value = params.prefilledHost; + this._urlField.hidden = !urlFieldVisible; + + await document.l10n.translateElements([ + permissionsText, + document.documentElement, + ]); + + document.getElementById("btnBlock").hidden = !params.blockVisible; + document.getElementById("btnSession").hidden = !params.sessionVisible; + 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 + ); + }, + + _getCapabilityL10nId(capability) { + let stringKey = null; + switch (capability) { + case Ci.nsIPermissionManager.ALLOW_ACTION: + stringKey = "permissions-capabilities-listitem-allow"; + break; + case Ci.nsIPermissionManager.DENY_ACTION: + stringKey = "permissions-capabilities-listitem-block"; + break; + case Ci.nsICookiePermission.ACCESS_SESSION: + stringKey = "permissions-capabilities-listitem-allow-session"; + break; + default: + throw new Error(`Unknown capability: ${capability}`); + } + return stringKey; + }, + + _addPermissionToList(perm) { + if (perm.type !== this._type) { + return; + } + if (!this._isCapabilitySupported(perm.capability)) { + 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; + } + + 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 richlistitem = document.createXULElement("richlistitem"); + richlistitem.setAttribute("origin", permission.origin); + let row = document.createXULElement("hbox"); + row.setAttribute("flex", "1"); + + let hbox = document.createXULElement("hbox"); + let website = document.createXULElement("label"); + website.setAttribute("value", permission.origin); + hbox.setAttribute("width", "0"); + hbox.setAttribute("class", "website-name"); + hbox.setAttribute("flex", "3"); + hbox.appendChild(website); + row.appendChild(hbox); + + if (!this._hideStatusColumn) { + hbox = document.createXULElement("hbox"); + let capability = document.createXULElement("label"); + capability.setAttribute("class", "website-capability-value"); + document.l10n.setAttributes( + capability, + this._getCapabilityL10nId(permission.capability) + ); + hbox.setAttribute("width", "0"); + hbox.setAttribute("class", "website-name"); + hbox.setAttribute("flex", "1"); + 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(); + } + } + }, + + onHostInput(siteField) { + this._btnSession.disabled = this._btnSession.hidden || !siteField.value; + this._btnBlock.disabled = this._btnBlock.hidden || !siteField.value; + this._btnAllow.disabled = this._btnAllow.hidden || !siteField.value; + }, + + _setRemoveButtonState() { + if (!this._list) { + return; + } + + let hasSelection = this._list.selectedIndex >= 0; + let hasRows = this._list.itemCount > 0; + this._removeButton.disabled = !hasSelection; + this._removeAllButton.disabled = !hasRows; + }, + + onPermissionDelete() { + let richlistitem = this._list.selectedItem; + let origin = richlistitem.getAttribute("origin"); + let permission = this._permissions.get(origin); + + this._removePermission(permission); + + this._setRemoveButtonState(); + }, + + onAllPermissionsDelete() { + for (let permission of this._permissions.values()) { + 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()) { + 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(); + }, + + _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..55e3f150de --- /dev/null +++ b/browser/components/preferences/dialogs/permissions.xhtml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" 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-window" + 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="browser/preferences/permissions.ftl"/> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/permissions.js"/> + + <keyset> + <key data-l10n-id="permissions-close-key" modifiers="accel" oncommand="window.close();"/> + </keyset> + + <vbox class="contentPane"> + <description id="permissionsText" control="url"/> + <separator class="thin"/> + <label id="urlLabel" control="url" data-l10n-id="permissions-address"/> + <hbox align="start"> + <html:input id="url" type="text" + style="-moz-box-flex: 1;" + oninput="gPermissionManager.onHostInput(event.target);" + onkeypress="gPermissionManager.onHostKeyPress(event);"/> + </hbox> + <hbox pack="end"> + <button id="btnBlock" disabled="true" data-l10n-id="permissions-block" + oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);"/> + <button id="btnSession" 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);"/> + </hbox> + <separator class="thin"/> + <listheader> + <treecol id="siteCol" data-l10n-id="permissions-site-name" flex="3" width="0" + onclick="gPermissionManager.buildPermissionsList(event.target)"/> + <treecol id="statusCol" data-l10n-id="permissions-status" flex="1" width="0" + data-isCurrentSortCol="true" + onclick="gPermissionManager.buildPermissionsList(event.target);"/> + </listheader> + <richlistbox id="permissionsBox" flex="1" selected="false" + onkeypress="gPermissionManager.onPermissionKeyPress(event);" + onselect="gPermissionManager.onPermissionSelect();"/> + </vbox> + + <hbox class="actionButtons"> + <button id="removePermission" disabled="true" + data-l10n-id="permissions-remove" + oncommand="gPermissionManager.onPermissionDelete();"/> + <button id="removeAllPermissions" + data-l10n-id="permissions-remove-all" + oncommand="gPermissionManager.onAllPermissionsDelete();"/> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/sanitize.js b/browser/components/preferences/dialogs/sanitize.js new file mode 100644 index 0000000000..f3f565c86e --- /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..d8a6c3c9d5 --- /dev/null +++ b/browser/components/preferences/dialogs/sanitize.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<!DOCTYPE window> + +<window id="SanitizeDialog" + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + persist="lastSelected" + data-l10n-id="sanitize-prefs" + 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-preferences" + preference="privacy.clearOnShutdown.siteSettings"/> + </vbox> + <vbox> + <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..e841df9c12 --- /dev/null +++ b/browser/components/preferences/dialogs/selectBookmark.js @@ -0,0 +1,118 @@ +//* -*- 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +XPCOMUtils.defineLazyModuleGetters(this, { + PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", + PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm", + PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", +}); +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..0c49389d9e --- /dev/null +++ b/browser/components/preferences/dialogs/selectBookmark.xhtml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="select-bookmark-window" + 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..2e7e6227ca --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.js @@ -0,0 +1,91 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { SiteDataManager } = ChromeUtils.import( + "resource:///modules/SiteDataManager.jsm" +); + +/** + * This dialog will ask the user to confirm that they really want to delete + * all site data for a number of hosts. Displaying the hosts can be done in + * two different ways by passing options in the arguments object. + * + * - Passing a "baseDomain" will cause the dialog to search for all applicable + * host names with that baseDomain using the SiteDataManager and populate the list + * asynchronously. As a side effect, the SiteDataManager will update. Thus it is + * safe to call SiteDataManager methods that require a manual updateSites call + * after spawning this dialog. However, you need to ensure that the SiteDataManager + * has finished updating. + * + * - Passing a "hosts" array allows you to manually specify the hosts to be displayed. + * The SiteDataManager will not be updated by spawning this dialog in that case. + * + * The two options are mutually exclusive. You must specify one. + **/ +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 baseDomain = window.arguments[0].baseDomain; + if (baseDomain) { + let hosts = new Set(); + SiteDataManager.updateSites((host, site) => { + // Disregard local files. + if (!host) { + return; + } + + if (site.baseDomain != baseDomain) { + return; + } + + // Avoid listing duplicate hosts. + if (hosts.has(host)) { + return; + } + hosts.add(host); + + let listItem = document.createXULElement("richlistitem"); + let label = document.createXULElement("label"); + label.setAttribute("value", host); + listItem.appendChild(label); + list.appendChild(listItem); + }); + return; + } + + let hosts = window.arguments[0].hosts; + if (hosts) { + 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); + return; + } + + throw new Error( + "Must specify either a hosts or baseDomain option in arguments." + ); + }, +}; diff --git a/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml new file mode 100644 index 0000000000..2d12f79ec7 --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/siteDataSettings.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/> + + <label data-l10n-id="site-data-removing-table"/> + <separator class="thin"/> + <richlistbox id="removalList" class="theme-listbox" 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..ab00219f63 --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataSettings.js @@ -0,0 +1,332 @@ +/* -*- 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.import( + "resource://gre/modules/AppConstants.jsm" +); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "SiteDataManager", + "resource:///modules/SiteDataManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm" +); + +let gSiteDataSettings = { + // Array of metadata of sites. Each array element is object holding: + // - uri: uri of site; instance of nsIURI + // - baseDomain: base domain of the site + // - cookies: array of cookies of that site + // - usage: disk usage which site uses + // - userAction: "remove" or "update-permission"; the action user wants to take. + _sites: null, + + _list: null, + _searchBox: null, + + _createSiteListItem(site) { + let item = document.createXULElement("richlistitem"); + item.setAttribute("host", site.host); + 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("flex", flexWidth); + let label = document.createXULElement("label"); + label.setAttribute("crop", "end"); + if (l10n) { + if (l10n.hasOwnProperty("raw")) { + box.setAttribute("tooltiptext", l10n.raw); + label.setAttribute("value", l10n.raw); + } else { + document.l10n.setAttributes(label, l10n.id, l10n.args); + } + } + if (tooltipText) { + box.setAttribute("tooltiptext", tooltipText); + } + box.appendChild(label); + container.appendChild(box); + } + + // Add "Host" column. + let hostData = site.host + ? { raw: site.host } + : { 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) { + let host = site.host; + if (keyword && !host.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 host = item.getAttribute("host"); + let siteForHost = this._sites.find(site => site.host == host); + if (siteForHost) { + siteForHost.userAction = "remove"; + } + item.remove(); + } + this._updateButtonsState(); + }, + + async saveChanges(event) { + let removals = this._sites + .filter(site => site.userAction == "remove") + .map(site => site.host); + + 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) { + Cu.reportError(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) + ) { + 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..d968d4028a --- /dev/null +++ b/browser/components/preferences/dialogs/siteDataSettings.xhtml @@ -0,0 +1,60 @@ +<?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/siteDataSettings.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="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 flex="4" width="50" data-l10n-id="site-data-column-host" id="hostCol"/> + <treecol flex="1" width="50" 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 flex="2" width="50" data-l10n-id="site-data-column-storage" id="usageCol" data-isCurrentSortCol="true"/> + <treecol flex="2" width="50" data-l10n-id="site-data-column-last-used" id="lastAccessedCol" /> + </listheader> + <richlistbox seltype="multiple" id="sitesList" orient="vertical" flex="1"/> + </vbox> + + <hbox align="start"> + <button id="removeSelected" data-l10n-id="site-data-remove-selected"/> + <button id="removeAll"/> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/dialogs/sitePermissions.css b/browser/components/preferences/dialogs/sitePermissions.css new file mode 100644 index 0000000000..547aedb3f3 --- /dev/null +++ b/browser/components/preferences/dialogs/sitePermissions.css @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.website-name { + overflow: hidden; /* Allows equal sizing combined with width="0" */ + padding-inline-start: 7px; + -moz-box-align: center; +} + +#permissionsBox { + min-height: 18em; +} + +#siteCol, +#statusCol, +#permissionsBox > richlistitem { + min-height: 35px; +} + +.website-status { + margin: 1px; + margin-inline-end: 5px; +} + +#browserNotificationsPermissionExtensionContent, +#permissionsDisableDescription { + margin-inline-start: 32px; +} + +#permissionsDisableDescription { + color: var(--in-content-deemphasized-text); + line-height: 110%; +} + +#permissionsDisableCheckbox { + margin-inline-start: 4px; + padding-top: 10px; +} diff --git a/browser/components/preferences/dialogs/sitePermissions.js b/browser/components/preferences/dialogs/sitePermissions.js new file mode 100644 index 0000000000..051329687f --- /dev/null +++ b/browser/components/preferences/dialogs/sitePermissions.js @@ -0,0 +1,611 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { SitePermissions } = ChromeUtils.import( + "resource:///modules/SitePermissions.jsm" +); + +const sitePermissionsL10n = { + "desktop-notification": { + window: "permissions-site-notification-window", + description: "permissions-site-notification-desc", + disableLabel: "permissions-site-notification-disable-label", + disableDescription: "permissions-site-notification-disable-desc", + }, + geo: { + window: "permissions-site-location-window", + description: "permissions-site-location-desc", + disableLabel: "permissions-site-location-disable-label", + disableDescription: "permissions-site-location-disable-desc", + }, + xr: { + window: "permissions-site-xr-window", + description: "permissions-site-xr-desc", + disableLabel: "permissions-site-xr-disable-label", + disableDescription: "permissions-site-xr-disable-desc", + }, + camera: { + window: "permissions-site-camera-window", + description: "permissions-site-camera-desc", + disableLabel: "permissions-site-camera-disable-label", + disableDescription: "permissions-site-camera-disable-desc", + }, + microphone: { + window: "permissions-site-microphone-window", + description: "permissions-site-microphone-desc", + disableLabel: "permissions-site-microphone-disable-label", + disableDescription: "permissions-site-microphone-disable-desc", + }, + "autoplay-media": { + window: "permissions-site-autoplay-window", + description: "permissions-site-autoplay-desc", + }, +}; + +const sitePermissionsConfig = { + "autoplay-media": { + _getCapabilityString(capability) { + switch (capability) { + case SitePermissions.ALLOW: + return "permissions-capabilities-autoplay-allow"; + case SitePermissions.BLOCK: + return "permissions-capabilities-autoplay-block"; + case SitePermissions.AUTOPLAY_BLOCKED_ALL: + return "permissions-capabilities-autoplay-blockall"; + } + throw new Error(`Unknown capability: ${capability}`); + }, + }, +}; + +function Permission(principal, type, capability, l10nId) { + this.principal = principal; + this.origin = principal.origin; + this.type = type; + this.capability = capability; + this.l10nId = l10nId; +} + +const PERMISSION_STATES = [ + SitePermissions.ALLOW, + SitePermissions.BLOCK, + SitePermissions.PROMPT, + SitePermissions.AUTOPLAY_BLOCKED_ALL, +]; + +const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled"; +const NOTIFICATIONS_PERMISSION_PREF = + "permissions.default.desktop-notification"; + +const AUTOPLAY_PREF = "media.autoplay.default"; + +var gSitePermissionsManager = { + _type: "", + _isObserving: false, + _permissions: new Map(), + _permissionsToChange: new Map(), + _permissionsToDelete: new Map(), + _list: null, + _removeButton: null, + _removeAllButton: null, + _searchBox: null, + _checkbox: null, + _currentDefaultPermissionsState: null, + _defaultPermissionStatePrefName: null, + + onLoad() { + let params = window.arguments[0]; + document.mozSubdialogReady = this.init(params); + }, + + async init(params) { + if (!this._isObserving) { + Services.obs.addObserver(this, "perm-changed"); + this._isObserving = true; + } + + document.addEventListener("dialogaccept", () => this.onApplyChanges()); + + this._type = params.permissionType; + this._list = document.getElementById("permissionsBox"); + this._removeButton = document.getElementById("removePermission"); + this._removeAllButton = document.getElementById("removeAllPermissions"); + this._searchBox = document.getElementById("searchBox"); + this._checkbox = document.getElementById("permissionsDisableCheckbox"); + this._disableExtensionButton = document.getElementById( + "disableNotificationsPermissionExtension" + ); + this._permissionsDisableDescription = document.getElementById( + "permissionsDisableDescription" + ); + this._setAutoplayPref = document.getElementById("setAutoplayPref"); + + let permissionsText = document.getElementById("permissionsText"); + + 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, + ]); + + // 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") { + this.buildAutoplayMenulist(); + this._setAutoplayPref.hidden = false; + } + + this._searchBox.focus(); + }, + + uninit() { + if (this._isObserving) { + Services.obs.removeObserver(this, "perm-changed"); + this._isObserving = false; + } + if (this._setAutoplayPref) { + this._setAutoplayPref.hidden = true; + } + }, + + observe(subject, topic, data) { + if (topic !== "perm-changed") { + return; + } + + let permission = subject.QueryInterface(Ci.nsIPermission); + + // Ignore unrelated permission types and permissions with unknown states. + if ( + permission.type !== this._type || + !PERMISSION_STATES.includes(permission.capability) + ) { + return; + } + + if (data == "added") { + this._addPermissionToList(permission); + this.buildPermissionsList(); + } else if (data == "changed") { + let p = this._permissions.get(permission.principal.origin); + p.capability = permission.capability; + p.l10nId = this._getCapabilityString( + permission.type, + permission.capability + ); + this._handleCapabilityChange(p); + this.buildPermissionsList(); + } else if (data == "deleted") { + this._removePermissionFromList(permission.principal.origin); + } + }, + + _handleCapabilityChange(perm) { + let permissionlistitem = document.getElementsByAttribute( + "origin", + perm.origin + )[0]; + let menulist = permissionlistitem.getElementsByTagName("menulist")[0]; + menulist.selectedItem = menulist.getElementsByAttribute( + "value", + perm.capability + )[0]; + }, + + _handleCheckboxUIUpdates() { + let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName); + if (pref != Services.prefs.PREF_INVALID) { + this._currentDefaultPermissionsState = Services.prefs.getIntPref( + this._defaultPermissionStatePrefName + ); + } + + if (this._currentDefaultPermissionsState === null) { + this._checkbox.setAttribute("hidden", true); + this._permissionsDisableDescription.setAttribute("hidden", true); + } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) { + this._checkbox.checked = true; + } else { + this._checkbox.checked = false; + } + + if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) { + this._checkbox.disabled = true; + } + }, + + /** + * Listen for changes to the permissions.default.* pref and make + * necessary changes to the UI. + */ + _watchPermissionPrefChange() { + this._handleCheckboxUIUpdates(); + + if (this._type == "desktop-notification") { + this._handleWebNotificationsDisable(); + + this._disableExtensionButton.addEventListener( + "command", + makeDisableControllingExtension( + PREF_SETTING_TYPE, + NOTIFICATIONS_PERMISSION_OVERRIDE_KEY + ) + ); + } + + let observer = () => { + this._handleCheckboxUIUpdates(); + if (this._type == "desktop-notification") { + this._handleWebNotificationsDisable(); + } + }; + Services.prefs.addObserver(this._defaultPermissionStatePrefName, observer); + window.addEventListener("unload", () => { + Services.prefs.removeObserver( + this._defaultPermissionStatePrefName, + observer + ); + }); + }, + + /** + * Handles the UI update for web notifications disable by extensions. + */ + async _handleWebNotificationsDisable() { + let prefLocked = Services.prefs.prefIsLocked(NOTIFICATIONS_PERMISSION_PREF); + if (prefLocked) { + // An extension can't control these settings if they're locked. + hideControllingExtension(NOTIFICATIONS_PERMISSION_OVERRIDE_KEY); + } else { + let isControlled = await handleControllingExtension( + PREF_SETTING_TYPE, + NOTIFICATIONS_PERMISSION_OVERRIDE_KEY + ); + this._checkbox.disabled = isControlled; + } + }, + + _getCapabilityString(type, capability) { + if ( + type in sitePermissionsConfig && + sitePermissionsConfig[type]._getCapabilityString + ) { + return sitePermissionsConfig[type]._getCapabilityString(capability); + } + + switch (capability) { + case Services.perms.ALLOW_ACTION: + return "permissions-capabilities-allow"; + case Services.perms.DENY_ACTION: + return "permissions-capabilities-block"; + case Services.perms.PROMPT_ACTION: + return "permissions-capabilities-prompt"; + default: + throw new Error(`Unknown capability: ${capability}`); + } + }, + + _addPermissionToList(perm) { + // Ignore unrelated permission types and permissions with unknown states. + if ( + perm.type !== this._type || + !PERMISSION_STATES.includes(perm.capability) || + // Skip private browsing session permissions + (perm.principal.privateBrowsingId !== + Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID && + perm.expireType === Services.perms.EXPIRE_SESSION) + ) { + return; + } + let l10nId = this._getCapabilityString(perm.type, perm.capability); + let p = new Permission(perm.principal, perm.type, perm.capability, l10nId); + this._permissions.set(p.origin, p); + }, + + _removePermissionFromList(origin) { + this._permissions.delete(origin); + let permissionlistitem = document.getElementsByAttribute( + "origin", + origin + )[0]; + if (permissionlistitem) { + permissionlistitem.remove(); + } + }, + + _loadPermissions() { + // load permissions into a table. + for (let nextPermission of Services.perms.all) { + this._addPermissionToList(nextPermission); + } + }, + + _createPermissionListItem(permission) { + let width = "75"; + let richlistitem = document.createXULElement("richlistitem"); + richlistitem.setAttribute("origin", permission.origin); + let row = document.createXULElement("hbox"); + row.setAttribute("flex", "1"); + + let hbox = document.createXULElement("hbox"); + let website = document.createXULElement("label"); + website.setAttribute("value", permission.origin); + website.setAttribute("width", width); + hbox.setAttribute("class", "website-name"); + hbox.setAttribute("flex", "3"); + hbox.appendChild(website); + + let menulist = document.createXULElement("menulist"); + menulist.setAttribute("flex", "1"); + menulist.setAttribute("width", width); + menulist.setAttribute("class", "website-status"); + let states = SitePermissions.getAvailableStates(permission.type); + for (let state of states) { + // Work around the (rare) edge case when a user has changed their + // default permission type back to UNKNOWN while still having a + // PROMPT permission set for an origin. + if ( + state == SitePermissions.UNKNOWN && + permission.capability == SitePermissions.PROMPT + ) { + state = SitePermissions.PROMPT; + } else if (state == SitePermissions.UNKNOWN) { + continue; + } + let m = menulist.appendItem(undefined, state); + document.l10n.setAttributes( + m, + this._getCapabilityString(permission.type, state) + ); + } + menulist.value = permission.capability; + + menulist.addEventListener("select", () => { + this.onPermissionChange(permission, Number(menulist.value)); + }); + + row.appendChild(hbox); + row.appendChild(menulist); + richlistitem.appendChild(row); + return richlistitem; + }, + + onPermissionKeyPress(event) { + if (!this._list.selectedItem) { + return; + } + + if ( + event.keyCode == KeyEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + event.keyCode == KeyEvent.DOM_VK_BACK_SPACE) + ) { + this.onPermissionDelete(); + event.preventDefault(); + } + }, + + _setRemoveButtonState() { + if (!this._list) { + return; + } + + let hasSelection = this._list.selectedIndex >= 0; + let hasRows = this._list.itemCount > 0; + this._removeButton.disabled = !hasSelection; + this._removeAllButton.disabled = !hasRows; + }, + + onPermissionDelete() { + let richlistitem = this._list.selectedItem; + let origin = richlistitem.getAttribute("origin"); + let permission = this._permissions.get(origin); + + this._removePermissionFromList(origin); + this._permissionsToDelete.set(permission.origin, permission); + + this._setRemoveButtonState(); + }, + + onAllPermissionsDelete() { + for (let permission of this._permissions.values()) { + this._removePermissionFromList(permission.origin); + this._permissionsToDelete.set(permission.origin, permission); + } + + this._setRemoveButtonState(); + }, + + onPermissionSelect() { + this._setRemoveButtonState(); + + // If any item is selected, it should be the only item tabable + // in the richlistbox for accessibility reasons. + this._list.itemChildren.forEach(item => { + let menulist = item.getElementsByTagName("menulist")[0]; + if (!item.selected) { + menulist.setAttribute("tabindex", -1); + } else { + menulist.removeAttribute("tabindex"); + } + }); + }, + + onPermissionChange(perm, capability) { + let p = this._permissions.get(perm.origin); + if (p.capability == capability) { + return; + } + p.capability = capability; + p.l10nId = this._getCapabilityString(perm.type, perm.capability); + this._permissionsToChange.set(p.origin, p); + + // enable "remove all" button as needed + this._setRemoveButtonState(); + }, + + onApplyChanges() { + // Stop observing permission changes since we are about + // to write out the pending adds/deletes and don't need + // to update the UI + this.uninit(); + + for (let p of this._permissionsToChange.values()) { + SitePermissions.setForPrincipal(p.principal, p.type, p.capability); + } + + for (let p of this._permissionsToDelete.values()) { + SitePermissions.removeFromPrincipal(p.principal, p.type); + } + + if (this._checkbox.checked) { + Services.prefs.setIntPref( + this._defaultPermissionStatePrefName, + SitePermissions.BLOCK + ); + } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) { + Services.prefs.setIntPref( + this._defaultPermissionStatePrefName, + SitePermissions.UNKNOWN + ); + } + }, + + buildPermissionsList(sortCol) { + // Clear old entries. + let oldItems = this._list.querySelectorAll("richlistitem"); + for (let item of oldItems) { + item.remove(); + } + let frag = document.createDocumentFragment(); + + let permissions = Array.from(this._permissions.values()); + + let keyword = this._searchBox.value.toLowerCase().trim(); + for (let permission of permissions) { + if (keyword && !permission.origin.includes(keyword)) { + continue; + } + + let richlistitem = this._createPermissionListItem(permission); + frag.appendChild(richlistitem); + } + + // Sort permissions. + this._sortPermissions(this._list, frag, sortCol); + + this._list.appendChild(frag); + + this._setRemoveButtonState(); + }, + + buildAutoplayMenulist() { + let menulist = document.createXULElement("menulist"); + let states = SitePermissions.getAvailableStates("autoplay-media"); + for (let state of states) { + let m = menulist.appendItem(undefined, state); + document.l10n.setAttributes( + m, + this._getCapabilityString("autoplay-media", state) + ); + } + + menulist.value = SitePermissions.getDefault("autoplay-media"); + + menulist.addEventListener("select", () => { + SitePermissions.setDefault("autoplay-media", Number(menulist.value)); + }); + + menulist.menupopup.setAttribute("incontentshell", "false"); + + menulist.disabled = Services.prefs.prefIsLocked(AUTOPLAY_PREF); + + document.getElementById("setAutoplayPref").appendChild(menulist); + }, + + _sortPermissions(list, frag, column) { + let sortDirection; + + if (!column) { + column = document.querySelector("treecol[data-isCurrentSortCol=true]"); + sortDirection = + column.getAttribute("data-last-sortDirection") || "ascending"; + } else { + sortDirection = column.getAttribute("data-last-sortDirection"); + sortDirection = + sortDirection === "ascending" ? "descending" : "ascending"; + } + + let sortFunc = null; + switch (column.id) { + case "siteCol": + sortFunc = (a, b) => { + return comp.compare( + a.getAttribute("origin"), + b.getAttribute("origin") + ); + }; + break; + + case "statusCol": + sortFunc = (a, b) => { + return ( + parseInt(a.querySelector("menulist").value) > + parseInt(b.querySelector("menulist").value) + ); + }; + break; + } + + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + let items = Array.from(frag.querySelectorAll("richlistitem")); + + if (sortDirection === "descending") { + items.sort((a, b) => sortFunc(b, a)); + } else { + items.sort(sortFunc); + } + + // Re-append items in the correct order: + items.forEach(item => frag.appendChild(item)); + + let cols = list.previousElementSibling.querySelectorAll("treecol"); + cols.forEach(c => { + c.removeAttribute("data-isCurrentSortCol"); + c.removeAttribute("sortDirection"); + }); + column.setAttribute("data-isCurrentSortCol", "true"); + column.setAttribute("sortDirection", sortDirection); + column.setAttribute("data-last-sortDirection", sortDirection); + }, +}; diff --git a/browser/components/preferences/dialogs/sitePermissions.xhtml b/browser/components/preferences/dialogs/sitePermissions.xhtml new file mode 100644 index 0000000000..e99758098c --- /dev/null +++ b/browser/components/preferences/dialogs/sitePermissions.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" 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-window" + 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" flex="3" width="75" + onclick="gSitePermissionsManager.buildPermissionsList(event.target)"/> + <treecol id="statusCol" data-l10n-id="permissions-status" flex="1" width="75" + data-isCurrentSortCol="true" + onclick="gSitePermissionsManager.buildPermissionsList(event.target);"/> + </listheader> + <richlistbox id="permissionsBox" flex="1" selected="false" + onkeypress="gSitePermissionsManager.onPermissionKeyPress(event);" + onselect="gSitePermissionsManager.onPermissionSelect();"/> + </vbox> + + <hbox class="actionButtons"> + <button id="removePermission" disabled="true" + data-l10n-id="permissions-remove" + oncommand="gSitePermissionsManager.onPermissionDelete();"/> + <button id="removeAllPermissions" + data-l10n-id="permissions-remove-all" + oncommand="gSitePermissionsManager.onAllPermissionsDelete();"/> + </hbox> + + <spacer flex="1"/> + <checkbox id="permissionsDisableCheckbox"/> + <description id="permissionsDisableDescription"/> + <spacer flex="1"/> + <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..67aaaf7593 --- /dev/null +++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.js @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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..3bd7f78940 --- /dev/null +++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml @@ -0,0 +1,63 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<window type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gSyncChooseWhatToSync.init();" + data-l10n-id="sync-choose-what-to-sync-dialog" + data-l10n-attrs="title, style"> +<dialog id="syncChooseOptions" + buttons="accept,cancel,extra2" + data-l10n-id="sync-choose-what-to-sync-dialog" + data-l10n-attrs="buttonlabelaccept, buttonlabelextra2"> + + <linkset> + <html:link rel="localization" href="browser/branding/sync-brand.ftl"/> + <html:link rel="localization" href="browser/preferences/preferences.ftl"/> + </linkset> + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js"/> + + <html:div class="sync-engines-list"> + <html:div class="sync-engine-bookmarks"> + <checkbox data-l10n-id="sync-engine-bookmarks" + preference="services.sync.engine.bookmarks"/> + </html:div> + <html:div class="sync-engine-history"> + <checkbox data-l10n-id="sync-engine-history" + preference="services.sync.engine.history"/> + </html:div> + <html:div class="sync-engine-tabs"> + <checkbox data-l10n-id="sync-engine-tabs" + preference="services.sync.engine.tabs"/> + </html:div> + <html:div class="sync-engine-passwords"> + <checkbox data-l10n-id="sync-engine-logins-passwords" + preference="services.sync.engine.passwords"/> + </html:div> + <html:div class="sync-engine-addresses"> + <checkbox data-l10n-id="sync-engine-addresses" + preference="services.sync.engine.addresses"/> + </html:div> + <html:div class="sync-engine-creditcards"> + <checkbox data-l10n-id="sync-engine-creditcards" + preference="services.sync.engine.creditcards"/> + </html:div> + <html:div class="sync-engine-addons"> + <checkbox data-l10n-id="sync-engine-addons" + preference="services.sync.engine.addons"/> + </html:div> + <html:div class="sync-engine-prefs"> + <checkbox data-l10n-id="sync-engine-prefs" + preference="services.sync.engine.prefs"/> + </html:div> + </html:div> +</dialog> +</window> diff --git a/browser/components/preferences/dialogs/translation.js b/browser/components/preferences/dialogs/translation.js new file mode 100644 index 0000000000..ca881143bb --- /dev/null +++ b/browser/components/preferences/dialogs/translation.js @@ -0,0 +1,258 @@ +/* -*- 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const kPermissionType = "translate"; +const kLanguagesPref = "browser.translation.neverForLanguages"; + +function Tree(aId, aData) { + this._data = aData; + this._tree = document.getElementById(aId); + this._tree.view = this; +} + +Tree.prototype = { + get tree() { + return this._tree; + }, + get isEmpty() { + return !this._data.length; + }, + get hasSelection() { + return this.selection.count > 0; + }, + getSelectedItems() { + let result = []; + + let rc = this.selection.getRangeCount(); + for (let i = 0; i < rc; ++i) { + let min = {}, + max = {}; + this.selection.getRangeAt(i, min, max); + for (let j = min.value; j <= max.value; ++j) { + result.push(this._data[j]); + } + } + + return result; + }, + + // nsITreeView implementation + get rowCount() { + return this._data.length; + }, + getCellText(aRow, aColumn) { + return this._data[aRow]; + }, + isSeparator(aIndex) { + return false; + }, + isSorted() { + return false; + }, + isContainer(aIndex) { + return false; + }, + setTree(aTree) {}, + getImageSrc(aRow, aColumn) {}, + getCellValue(aRow, aColumn) {}, + cycleHeader(column) {}, + getRowProperties(row) { + return ""; + }, + getColumnProperties(column) { + return ""; + }, + getCellProperties(row, column) { + return ""; + }, + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), +}; + +function Lang(aCode, label) { + this.langCode = aCode; + this._label = label; +} + +Lang.prototype = { + toString() { + return this._label; + }, +}; + +var gTranslationExceptions = { + onLoad() { + if (this._siteTree) { + // Re-using an open dialog, clear the old observers. + this.uninit(); + } + + // Load site permissions into an array. + this._sites = []; + for (let perm of Services.perms.all) { + if ( + perm.type == kPermissionType && + perm.capability == Services.perms.DENY_ACTION + ) { + this._sites.push(perm.principal.origin); + } + } + Services.obs.addObserver(this, "perm-changed"); + this._sites.sort(); + + this._siteTree = new Tree("sitesTree", this._sites); + this.onSiteSelected(); + + this._langs = this.getLanguageExceptions(); + Services.prefs.addObserver(kLanguagesPref, this); + this._langTree = new Tree("languagesTree", this._langs); + this.onLanguageSelected(); + }, + + // Get the list of languages we don't translate as an array. + getLanguageExceptions() { + let langs = Services.prefs.getCharPref(kLanguagesPref); + if (!langs) { + return []; + } + + let langArr = langs.split(","); + let displayNames = Services.intl.getLanguageDisplayNames( + undefined, + langArr + ); + let result = langArr.map((lang, i) => new Lang(lang, displayNames[i])); + result.sort(); + + return result; + }, + + observe(aSubject, aTopic, aData) { + if (aTopic == "perm-changed") { + if (aData == "cleared") { + if (!this._sites.length) { + return; + } + let removed = this._sites.splice(0, this._sites.length); + this._siteTree.tree.rowCountChanged(0, -removed.length); + } else { + let perm = aSubject.QueryInterface(Ci.nsIPermission); + if (perm.type != kPermissionType) { + return; + } + + if (aData == "added") { + if (perm.capability != Services.perms.DENY_ACTION) { + return; + } + this._sites.push(perm.principal.origin); + this._sites.sort(); + let tree = this._siteTree.tree; + tree.rowCountChanged(0, 1); + tree.invalidate(); + } else if (aData == "deleted") { + let index = this._sites.indexOf(perm.principal.origin); + if (index == -1) { + return; + } + this._sites.splice(index, 1); + this._siteTree.tree.rowCountChanged(index, -1); + this.onSiteSelected(); + return; + } + } + this.onSiteSelected(); + } else if (aTopic == "nsPref:changed") { + this._langs = this.getLanguageExceptions(); + let change = this._langs.length - this._langTree.rowCount; + this._langTree._data = this._langs; + let tree = this._langTree.tree; + if (change) { + tree.rowCountChanged(0, change); + } + tree.invalidate(); + this.onLanguageSelected(); + } + }, + + _handleButtonDisabling(aTree, aIdPart) { + let empty = aTree.isEmpty; + document.getElementById("removeAll" + aIdPart + "s").disabled = empty; + document.getElementById("remove" + aIdPart).disabled = + empty || !aTree.hasSelection; + }, + + onLanguageSelected() { + this._handleButtonDisabling(this._langTree, "Language"); + }, + + onSiteSelected() { + this._handleButtonDisabling(this._siteTree, "Site"); + }, + + onLanguageDeleted() { + let langs = Services.prefs.getCharPref(kLanguagesPref); + if (!langs) { + return; + } + + let removed = this._langTree.getSelectedItems().map(l => l.langCode); + + langs = langs.split(",").filter(l => !removed.includes(l)); + Services.prefs.setCharPref(kLanguagesPref, langs.join(",")); + }, + + onAllLanguagesDeleted() { + Services.prefs.setCharPref(kLanguagesPref, ""); + }, + + onSiteDeleted() { + let removedSites = this._siteTree.getSelectedItems(); + for (let origin of removedSites) { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + Services.perms.removeFromPrincipal(principal, kPermissionType); + } + }, + + onAllSitesDeleted() { + if (this._siteTree.isEmpty) { + return; + } + + let removedSites = this._sites.splice(0, this._sites.length); + this._siteTree.tree.rowCountChanged(0, -removedSites.length); + + for (let origin of removedSites) { + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + Services.perms.removeFromPrincipal(principal, kPermissionType); + } + + this.onSiteSelected(); + }, + + onSiteKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onSiteDeleted(); + } + }, + + onLanguageKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onLanguageDeleted(); + } + }, + + uninit() { + Services.obs.removeObserver(this, "perm-changed"); + Services.prefs.removeObserver(kLanguagesPref, this); + }, +}; diff --git a/browser/components/preferences/dialogs/translation.xhtml b/browser/components/preferences/dialogs/translation.xhtml new file mode 100644 index 0000000000..c092e5dae2 --- /dev/null +++ b/browser/components/preferences/dialogs/translation.xhtml @@ -0,0 +1,87 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<window id="TranslationDialog" + data-l10n-id="translation-window" + data-l10n-attrs="title, style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gTranslationExceptions.onLoad();" + onunload="gTranslationExceptions.uninit();" + persist="width height"> + + <dialog + buttons="accept" + data-l10n-id="translation-dialog" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"> + + <linkset> + <html:link rel="localization" href="browser/preferences/translation.ftl"/> + </linkset> + + <script src="chrome://browser/content/preferences/dialogs/translation.js"/> + + <keyset> + <key data-l10n-id="translation-close-key" modifiers="accel" oncommand="window.close();"/> + </keyset> + + <vbox class="contentPane"> + <vbox flex="1"> + <label id="languagesLabel" + data-l10n-id="translation-languages-disabled-desc" + control="permissionsTree"/> + <separator class="thin"/> + <tree id="languagesTree" flex="1" style="height: 12em;" + hidecolumnpicker="true" + onkeypress="gTranslationExceptions.onLanguageKeyPress(event)" + onselect="gTranslationExceptions.onLanguageSelected();"> + <treecols> + <treecol id="languageCol" data-l10n-id="translation-languages-column" flex="1"/> + </treecols> + <treechildren/> + </tree> + </vbox> + <hbox class="actionButtons" pack="end"> + <button id="removeLanguage" disabled="true" + data-l10n-id="translation-languages-button-remove" + oncommand="gTranslationExceptions.onLanguageDeleted();"/> + <button id="removeAllLanguages" + data-l10n-id="translation-languages-button-remove-all" + oncommand="gTranslationExceptions.onAllLanguagesDeleted();"/> + <spacer flex="1"/> + </hbox> + <separator/> + <vbox flex="1"> + <label id="languagesLabel" + data-l10n-id="translation-sites-disabled-desc" + control="permissionsTree"/> + <separator class="thin"/> + <tree id="sitesTree" flex="1" style="height: 12em;" + hidecolumnpicker="true" + onkeypress="gTranslationExceptions.onSiteKeyPress(event)" + onselect="gTranslationExceptions.onSiteSelected();"> + <treecols> + <treecol id="siteCol" data-l10n-id="translation-sites-column" flex="1"/> + </treecols> + <treechildren/> + </tree> + </vbox> + </vbox> + + <hbox class="actionButtons" pack="end"> + <button id="removeSite" disabled="true" + data-l10n-id="translation-sites-button-remove" + oncommand="gTranslationExceptions.onSiteDeleted();"/> + <button id="removeAllSites" + data-l10n-id="translation-sites-button-remove-all" + oncommand="gTranslationExceptions.onAllSitesDeleted();"/> + <spacer flex="1"/> + </hbox> + </dialog> +</window> diff --git a/browser/components/preferences/experimental.inc.xhtml b/browser/components/preferences/experimental.inc.xhtml new file mode 100644 index 0000000000..cbc28b98ed --- /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="-moz-box-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-description"/> +</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..55a2b0c3f3 --- /dev/null +++ b/browser/components/preferences/experimental.js @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { FeatureGate } = ChromeUtils.import( + "resource://featuregates/FeatureGate.jsm" +); + +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); + Preferences.updateAllElements(); + }, +}; diff --git a/browser/components/preferences/extensionControlled.js b/browser/components/preferences/extensionControlled.js new file mode 100644 index 0000000000..2baac62d3a --- /dev/null +++ b/browser/components/preferences/extensionControlled.js @@ -0,0 +1,318 @@ +/* - This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from preferences.js */ + +"use strict"; + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "DeferredTask", + "resource://gre/modules/DeferredTask.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ExtensionSettingsStore", + "resource://gre/modules/ExtensionSettingsStore.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ExtensionPreferencesManager", + "resource://gre/modules/ExtensionPreferencesManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "Management", + "resource://gre/modules/Extension.jsm" +); + +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.ftp", + "network.proxy.ftp_port", + "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-controlled-${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.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.className = "extension-controlled-icon"; + return img; + }; + let label = document.createXULElement("label"); + let addonIcon = icon( + "chrome://mozapps/skin/extensions/extension.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); + controlledElement.disabled = managementControlled; + }; + 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..3770598aa6 --- /dev/null +++ b/browser/components/preferences/findInPage.js @@ -0,0 +1,709 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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(), + + init() { + if (this.inited) { + return; + } + this.inited = true; + this.searchInput = document.getElementById("searchInput"); + this.searchInput.hidden = !Services.prefs.getBoolPref( + "browser.preferences.search" + ); + if (!this.searchInput.hidden) { + this.searchInput.addEventListener("input", this); + this.searchInput.addEventListener("command", this); + window.addEventListener("DOMContentLoaded", () => { + this.searchInput.focus(); + }); + // Initialize other panes in an idle callback. + window.requestIdleCallback(() => this.initializeCategories()); + } + let helpUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "preferences"; + let helpContainer = document.getElementById("need-help"); + helpContainer.querySelector("a").href = helpUrl; + }, + + async handleEvent(event) { + // Ensure categories are initialized if idle callback didn't run sooo enough. + await this.initializeCategories(); + this.searchFunction(event); + }, + + /** + * Check that the text content contains the query string. + * + * @param String content + * the text content to be searched + * @param String query + * the query string + * @returns boolean + * true when the text content contains the query string else false + */ + queryMatchesContent(content, query) { + if (!content || !query) { + return false; + } + return content.toLowerCase().includes(query.toLowerCase()); + }, + + categoriesInitialized: false, + + /** + * Will attempt to initialize all uninitialized categories + */ + async initializeCategories() { + // Initializing all the JS for all the tabs + if (!this.categoriesInitialized) { + this.categoriesInitialized = true; + // Each element of gCategoryInits is a name + for (let [, /* name */ category] of gCategoryInits) { + if (!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); + } + + 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 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) { + // 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 { + 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, add it to the list + // of subitems. The items that don't match the search term will be hidden. + if (child instanceof Element && child.classList.contains("featureGate")) { + 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.calculateTooltipPosition(anchorNode); + }, + + calculateTooltipPosition(anchorNode) { + let searchTooltip = anchorNode.tooltipNode; + // In order to get the up-to-date position of each of the nodes that we're + // putting tooltips on, we have to flush layout intentionally, and that + // this is the result of a XUL limitation (bug 1363730). + let tooltipRect = searchTooltip.getBoundingClientRect(); + searchTooltip.style.setProperty( + "left", + `calc(50% - ${tooltipRect.width / 2}px)` + ); + }, + + /** + * Remove all search indicators. This would be called when switching away from + * a search to another preference category. + */ + removeAllSearchIndicators(window, showSubItems) { + this.getFindSelection(window).removeAllRanges(); + 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..42fad9f8e3 --- /dev/null +++ b/browser/components/preferences/fxaPairDevice.js @@ -0,0 +1,124 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { FxAccounts } = ChromeUtils.import( + "resource://gre/modules/FxAccounts.jsm" +); +const { Weave } = ChromeUtils.import("resource://services-sync/main.js"); + +XPCOMUtils.defineLazyModuleGetters(this, { + EventEmitter: "resource://gre/modules/EventEmitter.jsm", + FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.jsm", +}); +const { require } = ChromeUtils.import( + "resource://devtools/shared/Loader.jsm", + {} +); +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(); + FxAccounts.config + .promiseConnectDeviceURI("pairing-modal") + .then(connectURI => { + document + .getElementById("connect-another-device-link") + .setAttribute("href", connectURI); + }); + // We let the modal show itself before eventually showing a master-password dialog later. + Services.tm.dispatchToMainThread(() => this.startPairingFlow()); + }, + + uninit() { + 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."); + } + 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); + } + }, + + _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) { + Cu.reportError(err); + this.teardownListeners(); + document + .getElementById("qrWrapper") + .setAttribute("pairing-status", "error"); + }, + + _switchToUrl(url) { + const browser = window.docShell.chromeEventHandler; + browser.loadURI(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..1c23207662 --- /dev/null +++ b/browser/components/preferences/fxaPairDevice.xhtml @@ -0,0 +1,61 @@ +<?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" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + role="dialog" + onload="gFxaPairDeviceDialog.init();" + onunload="gFxaPairDeviceDialog.uninit()" + data-l10n-id="fxa-pair-device-dialog" + data-l10n-attrs="title, style"> + + <linkset> + <html:link rel="localization" href="browser/branding/sync-brand.ftl"/> + <html:link rel="localization" href="browser/preferences/fxaPairDevice.ftl"/> + </linkset> + <script src="chrome://browser/content/preferences/fxaPairDevice.js"/> + + <vbox id="qrCodeDisplay"> + <description class="pairHeading" data-l10n-id="fxa-qrcode-heading-step1"> + <html:a + id="connect-another-device-link" + data-l10n-name="connect-another-device" + class="text-link" target="_blank"/> + </description> + <description class="pairHeading" data-l10n-id="fxa-qrcode-heading-step2"></description> + <description class="pairHeading" data-l10n-id="fxa-qrcode-heading-step3"> + <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"/> + <html:img + src="chrome://global/skin/icons/settings.svg" + data-l10n-name="settings-icon" + class="menu-icon"/> + </description> + <description class="pairHeading" data-l10n-id="fxa-qrcode-heading-step4"></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> +</window> diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml new file mode 100644 index 0000000000..c348e1cf75 --- /dev/null +++ b/browser/components/preferences/home.inc.xhtml @@ -0,0 +1,95 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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="-moz-box-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 class="homepageMenuItemContainer" 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" /> + <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="-moz-box-flex: 1;" + data-preference-related="browser.startup.homepage" + data-l10n-id="home-homepage-custom-url" + autocompletepopup="homePageUrlAutocomplete" + autocompletesearch="unifiedcomplete" /> + <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" + flex="1" + 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" + flex="1" + 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-window.title, select-bookmark-desc"/> + </hbox> + </vbox> + </vbox> + </hbox> + <vbox data-subcategory="newtabOverride"> + <hbox id="newTabsOption" align="center"> + <label control="newTabMode" data-l10n-id="home-newtabs-mode-label" 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" /> + <menuitem value="1" data-l10n-id="home-mode-choice-blank" /> + </menupopup> + </menulist> + </hbox> + </vbox> +</groupbox> +</html:template> diff --git a/browser/components/preferences/home.js b/browser/components/preferences/home.js new file mode 100644 index 0000000000..f5b67ccfa2 --- /dev/null +++ b/browser/components/preferences/home.js @@ -0,0 +1,712 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" }, +]); + +XPCOMUtils.defineLazyModuleGetters(this, { + ExtensionPreferencesManager: + "resource://gre/modules/ExtensionPreferencesManager.jsm", +}); + +const HOMEPAGE_OVERRIDE_KEY = "homepage_override"; +const URL_OVERRIDES_TYPE = "url_overrides"; +const NEW_TAB_KEY = "newTabURL"; + +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" + ) { + 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; + } + // 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 ( + homePage === "about:blank" || + (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", ""].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 (HomePage.get() !== "about:blank") { + HomePage.safeSet("about:blank"); + } 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(Cu.reportError); + } + + Services.telemetry.scalarAdd("preferences.use_current_page", 1); + }, + + _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(Cu.reportError); + } + }, + + /** + * 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 + ); + Services.telemetry.scalarAdd("preferences.use_bookmark", 1); + }, + + restoreDefaultHomePage() { + HomePage.reset(); + this._handleHomePageOverrides(); + Services.prefs.clearUserPref(this.NEWTAB_ENABLED_PREF); + AboutNewTab.resetNewTabURL(); + }, + + onCustomHomePageInput(event) { + if (this._telemetryHomePageTimer) { + clearTimeout(this._telemetryHomePageTimer); + } + let browserHomePage = event.target.value; + // The length of the home page URL string should be more then four, + // and it should contain at least one ".", for example, "https://mozilla.org". + if (browserHomePage.length > 4 && browserHomePage.includes(".")) { + this._telemetryHomePageTimer = setTimeout(() => { + let homePageNumber = browserHomePage.split("|").length; + Services.telemetry.scalarAdd("preferences.browser_home_page_change", 1); + Services.telemetry.keyedScalarAdd( + "preferences.browser_home_page_count", + homePageNumber, + 1 + ); + }, 3000); + } + }, + + onCustomHomePageChange(event) { + const value = event.target.value || HomePage.getDefault(); + HomePage.set(value).catch(Cu.reportError); + }, + + /** + * 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 + ); + }, + + /** + * 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(); + btn.style.visibility = prefChanged ? "visible" : "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("homePageUrl") + .addEventListener("input", this.onCustomHomePageInput.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(); + 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..4f3babeaf8 --- /dev/null +++ b/browser/components/preferences/jar.mn @@ -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/. + +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/fxaPairDevice.xhtml + content/browser/preferences/fxaPairDevice.js + content/browser/preferences/findInPage.js diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml new file mode 100644 index 0000000000..ec30d31cde --- /dev/null +++ b/browser/components/preferences/main.inc.xhtml @@ -0,0 +1,723 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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-previous-session"/> + <hbox class="indent"> + <checkbox id="browserRestoreSessionQuitWarning" + preference="browser.sessionstore.warnOnQuit" + disabled="true" + data-l10n-id="startup-restore-warn-on-quit"/> + </hbox> + </vbox> + +#ifdef HAVE_SHELL_SERVICE + <vbox id="defaultBrowserBox"> + <checkbox id="alwaysCheckDefault" preference="browser.shell.checkDefaultBrowser" + disabled="true" + data-l10n-id="always-check-default"/> + <deck id="setDefaultPane"> + <hbox align="center" class="indent"> + <image class="face-sad"/> + <label 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 align="center" class="indent"> + <image class="face-smile"/> + <label id="isDefaultLabel" flex="1" data-l10n-id="is-default"/> + </hbox> + </deck> + </vbox> +#endif + +</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.recentlyUsedOrder"/> + + <checkbox id="linkTargeting" data-l10n-id="open-new-link-as-tabs" + preference="browser.link.open_newwindow"/> + + <checkbox id="warnCloseMultiple" data-l10n-id="warn-on-close-multiple-tabs" + preference="browser.tabs.warnOnClose"/> + + <checkbox id="warnOpenMany" data-l10n-id="warn-on-open-many-tabs" + preference="browser.tabs.warnOnOpen"/> + + <checkbox id="switchToNewTabs" data-l10n-id="switch-links-to-new-tabs" + preference="browser.tabs.loadInBackground"/> + +#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"> + <description control="disableContainersExtension" flex="1" /> + <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"/> + <label id="browserContainersLearnMore" is="text-link" class="learnMore" data-l10n-id="browser-containers-learn-more"/> + <spacer flex="1"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <button id="browserContainersSettings" + is="highlightable-button" + class="accessory-button" + data-l10n-id="browser-containers-settings" + search-l10n-ids="containers-add-button.label, + containers-preferences-button.label, + containers-remove-button.label" + /> + </hbox> + </hbox> + </vbox> +</groupbox> + +<hbox id="languageAndAppearanceCategory" + class="subcategory" + hidden="true" + data-category="paneGeneral"> + <html:h1 data-l10n-id="language-and-appearance-header"/> +</hbox> + +<!-- Fonts and Colors --> +<groupbox id="fontsGroup" data-category="paneGeneral" hidden="true"> + <label><html:h2 data-l10n-id="fonts-and-colors-header"/></label> + + <hbox id="fontSettings"> + <hbox align="center" flex="1"> + <label control="defaultFont" data-l10n-id="default-font"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox flex="1"> + <menulist id="defaultFont" flex="1" delayprefsave="true"/> + </hbox> + <label id="defaultFontSizeLabel" control="defaultFontSize" data-l10n-id="default-font-size"></label> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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> + </hbox> + + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <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> + </hbox> + <hbox id="colorsSettings"> + <spacer flex="1" /> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <button id="colors" + is="highlightable-button" + class="accessory-button" + data-l10n-id="colors-settings" + 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> + </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"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <menulist id="defaultZoom"> + <menupopup/> + </menulist> + </hbox> + </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="defaultBrowserLanguage"> + <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"> + <image 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"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <button id="chooseLanguage" + is="highlightable-button" + class="accessory-button" + data-l10n-id="choose-button" + search-l10n-ids=" + webpage-languages-window.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> + </hbox> + + <checkbox id="useSystemLocale" hidden="true" + data-l10n-id="use-system-locale" + data-l10n-args='{"localeName": "und"}' + preference="intl.regional_prefs.use_os_locales"/> + + <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> + <checkbox id="checkSpelling" + data-l10n-id="check-user-spelling" + preference="layout.spellcheckDefault"/> +</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> + + <radiogroup id="saveWhere" + preference="browser.download.useDownloadDir"> + <hbox> + <radio id="saveTo" + value="true" + data-l10n-id="download-save-to"/> + <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> + <!-- Additional radio button added to support CloudStorage - Bug 1357171 --> + <radio id="saveToCloud" + value="true" + hidden="true"/> + <radio id="alwaysAsk" + value="false" + data-l10n-id="download-always-ask-where"/> + </radiogroup> +</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 equalsize="always"> + <treecol id="typeColumn" data-l10n-id="applications-type-column" value="type" + persist="sortDirection" + flex="1" sortDirection="ascending"/> + <treecol id="actionColumn" data-l10n-id="applications-action-column" value="action" + persist="sortDirection" + flex="1"/> + </listheader> + <richlistbox id="handlersView" + preference="pref.downloads.disable_button.edit_actions"/> +</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" /> + <label id="playDRMContentLink" class="learnMore" data-l10n-id="play-drm-content-learn-more" is="text-link"/> + </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 + <spacer flex="1"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <vbox> + <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 + "/> + </vbox> +#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"> + <image class="update-throbber"/> + <label data-l10n-id="update-checkingForUpdates"></label> + <spacer flex="1"/> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="downloading" align="start" data-l10n-id="update-downloading"> + <html:img class="update-throbber" src="chrome://global/skin/icons/loading.png" data-l10n-name="icon"/> + <label id="downloadStatus" data-l10n-name="download-status"/> + </hbox> + <hbox id="applying" align="start"> + <image class="update-throbber"/> + <label data-l10n-id="update-applying"></label> + </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> + <spacer flex="1"/> + <button id="checkForUpdatesButton2" + data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button"/> + </hbox> + <hbox id="policyDisabled" align="start"> + <label data-l10n-id="update-adminDisabled"></label> + <spacer flex="1"/> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="noUpdatesFound" align="start"> + <image class="face-smile"/> + <label data-l10n-id="update-noUpdatesFound"></label> + <spacer flex="1"/> + <button id="checkForUpdatesButton3" + data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button"/> + </hbox> + <hbox id="otherInstanceHandlingUpdates" align="start"> + <label data-l10n-id="update-otherInstanceHandlingUpdates"></label> + <spacer flex="1"/> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="manualUpdate" align="start"> + <image class="face-sad"/> + <description flex="1" data-l10n-id="update-manual"> + <label id="manualLink" data-l10n-name="manual-link" is="text-link"/> + </description> + <spacer flex="1"/> + <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> + <spacer flex="1"/> + <button data-l10n-id="update-checkForUpdatesButton" + is="highlightable-button" + disabled="true"/> + </hbox> + <hbox id="restarting" align="start"> + <image class="update-throbber"/><label data-l10n-id="update-restarting"></label> + <spacer flex="1"/> + <button data-l10n-id="update-updateButton" + 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"> + <radiogroup id="updateRadioGroup"> + <radio id="autoDesktop" + value="true" + data-l10n-id="update-application-auto"/> + <radio id="manualDesktop" + value="false" + data-l10n-id="update-application-check-choose"/> + </radiogroup> + <description id="updateSettingCrossUserWarning" hidden="true" + data-l10n-id="update-application-warning-cross-user-setting"> + </description> + </vbox> +#ifdef MOZ_MAINTENANCE_SERVICE + <checkbox id="useService" + data-l10n-id="update-application-use-service" + preference="app.update.service.enabled"/> +#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"/> + <label id="performanceSettingsLearnMore" class="learnMore" data-l10n-id="performance-settings-learn-more" is="text-link"/> + </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 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"/> + <label id="pictureInPictureLearnMore" class="learnMore" data-l10n-id="browsing-picture-in-picture-learn-more" is="text-link"/> + </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"/> + <label id="mediaControlLearnMore" class="learnMore" data-l10n-id="browsing-media-control-learn-more" is="text-link"/> + </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"/> + <label id="cfrLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/> + </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"/> + <label id="cfrFeaturesLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/> + </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"> + <hbox align="center" flex="1"> + <description id="connectionSettingsDescription" control="connectionSettings"/> + <spacer width="5"/> + <label id="connectionSettingsLearnMore" class="learnMore" is="text-link" + data-l10n-id="network-proxy-connection-learn-more"> + </label> + <separator orient="vertical"/> + </hbox> + + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <button id="connectionSettings" + is="highlightable-button" + class="accessory-button" + data-l10n-id="network-proxy-connection-settings" + searchkeywords="doh trr" + search-l10n-ids=" + connection-window.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-ftp, + connection-proxy-http-port, + connection-proxy-socks, + connection-proxy-socks4, + connection-proxy-socks5, + connection-proxy-noproxy, + connection-proxy-noproxy-desc, + connection-proxy-http-sharing.label, + connection-proxy-autotype.label, + connection-proxy-reload.label, + connection-proxy-autologin.label, + connection-proxy-socks-remote-dns.label, + connection-dns-over-https.label, + connection-dns-over-https-url-custom.label, + " /> + </hbox> + </hbox> +</groupbox> +</html:template> diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js new file mode 100644 index 0000000000..acb15e6119 --- /dev/null +++ b/browser/components/preferences/main.js @@ -0,0 +1,3472 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 ../../base/content/aboutDialog-appUpdater.js */ +/* global MozXULElement */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { Downloads } = ChromeUtils.import("resource://gre/modules/Downloads.jsm"); +var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); +var { TransientPrefs } = ChromeUtils.import( + "resource:///modules/TransientPrefs.jsm" +); +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +var { L10nRegistry } = ChromeUtils.import( + "resource://gre/modules/L10nRegistry.jsm" +); +var { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm"); +ChromeUtils.defineModuleGetter( + this, + "CloudStorage", + "resource://gre/modules/CloudStorage.jsm" +); +var { Integration } = ChromeUtils.import( + "resource://gre/modules/Integration.jsm" +); +/* global DownloadIntegration */ +Integration.downloads.defineModuleGetter( + this, + "DownloadIntegration", + "resource://gre/modules/DownloadIntegration.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "SelectionChangedMenulist", + "resource:///modules/SelectionChangedMenulist.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetters(this, { + gHandlerService: [ + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService", + ], + gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"], +}); + +// 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 AUTO_UPDATE_CHANGED_TOPIC = "auto-update-config-change"; + +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"; + +ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); + +if (AppConstants.MOZ_DEV_EDITION) { + ChromeUtils.defineModuleGetter( + this, + "fxAccounts", + "resource://gre/modules/FxAccounts.jsm" + ); + ChromeUtils.defineModuleGetter( + this, + "FxAccounts", + "resource://gre/modules/FxAccounts.jsm" + ); +} + +Preferences.addAll([ + // Startup + { id: "browser.startup.page", type: "int" }, + { id: "browser.privatebrowsing.autostart", type: "bool" }, + { id: "browser.sessionstore.warnOnQuit", type: "bool" }, + + // Downloads + { id: "browser.download.useDownloadDir", 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.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.tabs.warnOnOpen", type: "bool" }, + { id: "browser.ctrlTab.recentlyUsedOrder", 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: "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.MOZ_MAINTENANCE_SERVICE) { + Preferences.addAll([{ id: "app.update.service.enabled", type: "bool" }]); + } +} + +// 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, + ]) + ); + function generateBundles(resourceIds) { + return L10nRegistry.generateBundles(locales, resourceIds); + } + return new Localization( + ["browser/preferences/preferences.ftl", "branding/brand.ftl"], + false, + { generateBundles } + ); +} + +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(); + + let performanceSettingsLink = document.getElementById( + "performanceSettingsLearnMore" + ); + let performanceSettingsUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "performance"; + performanceSettingsLink.setAttribute("href", performanceSettingsUrl); + + this.updateDefaultPerformanceSettingsPref(); + + let defaultPerformancePref = Preferences.get( + "browser.preferences.defaultPerformanceSettings.enabled" + ); + defaultPerformancePref.on("change", () => { + this.updatePerformanceSettingsBox({ duringChangeEvent: true }); + }); + this.updatePerformanceSettingsBox({ duringChangeEvent: false }); + this.displayUseSystemLocale(); + let connectionSettingsLink = document.getElementById( + "connectionSettingsLearnMore" + ); + let connectionSettingsUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "prefs-connection-settings"; + connectionSettingsLink.setAttribute("href", connectionSettingsUrl); + this.updateProxySettingsUI(); + initializeProxyUI(gMainPane); + + if (Services.prefs.getBoolPref("intl.multilingual.enabled")) { + gMainPane.initBrowserLocale(); + } + + // We call `initDefaultZoomValues` to set and unhide the + // default zoom preferences menu, and to establish a + // listener for future menu changes. + gMainPane.initDefaultZoomValues(); + + let cfrLearnMoreUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "extensionrecommendations"; + for (const id of ["cfrLearnMore", "cfrFeaturesLearnMore"]) { + let link = document.getElementById(id); + link.setAttribute("href", cfrLearnMoreUrl); + } + + if ( + Services.prefs.getBoolPref( + "media.videocontrols.picture-in-picture.enabled" + ) + ) { + document.getElementById("pictureInPictureBox").hidden = false; + + let pipLearnMoreUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "picture-in-picture"; + let link = document.getElementById("pictureInPictureLearnMore"); + link.setAttribute("href", pipLearnMoreUrl); + } + + 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 "closing multiple tabs" and "opening multiple tabs might slow down + // &brandShortName;" warnings provide options for not showing these + // warnings again. When the user disabled them, we provide checkboxes to + // re-enable the warnings. + if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnClose")) { + document.getElementById("warnCloseMultiple").hidden = true; + } + if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen")) { + document.getElementById("warnOpenMany").hidden = true; + } + + setEventListener("ctrlTabRecentlyUsedOrder", "command", function() { + Services.prefs.clearUserPref("browser.ctrlTab.migrated"); + }); + setEventListener("manageBrowserLanguagesButton", "command", function() { + gMainPane.showBrowserLanguages({ 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(); + }); + } + + // 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 + ); + setEventListener( + "translateButton", + "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 + ); + + // 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; + let mediaControlLearnMoreUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "media-keyboard-control"; + let link = document.getElementById("mediaControlLearnMore"); + link.setAttribute("href", mediaControlLearnMoreUrl); + setEventListener( + "mediaControlToggleEnabled", + "command", + gMainPane.updateMediaControlTelemetry + ); + } + + // Initializes the fonts dropdowns displayed in this pane. + this._rebuildFonts(); + + this.updateOnScreenKeyboardVisibility(); + + // Show translation preferences if we may: + const prefName = "browser.translation.ui.show"; + if (Services.prefs.getBoolPref(prefName)) { + 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"); + } + } + + let drmInfoURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "drm-content"; + document + .getElementById("playDRMContentLink") + .setAttribute("href", drmInfoURL); + 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 distroId = Services.prefs.getCharPref("distribution.id", ""); + if (distroId) { + let distroString = distroId; + + let distroVersion = Services.prefs.getCharPref( + "distribution.version", + "" + ); + if (distroVersion) { + distroString += " - " + distroVersion; + } + + let distroIdField = document.getElementById("distributionId"); + distroIdField.value = distroString; + distroIdField.hidden = false; + + let distroAbout = Services.prefs.getStringPref("distribution.about", ""); + if (distroAbout) { + let distroField = document.getElementById("distribution"); + distroField.value = distroAbout; + distroField.hidden = false; + } + } + + if (AppConstants.MOZ_UPDATER) { + // XXX Workaround bug 1523453 -- changing selectIndex of a <deck> before + // frame construction could confuse nsDeckFrame::RemoveFrame(). + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + gAppUpdater = new appUpdater(); + }); + }); + setEventListener("showUpdateHistory", "command", gMainPane.showUpdates); + + let updateDisabled = + Services.policies && !Services.policies.isAllowed("appUpdate"); + if (updateDisabled || UpdateUtils.appUpdateAutoSettingIsLocked()) { + document.getElementById("updateAllowDescription").hidden = true; + document.getElementById("updateSettingsContainer").hidden = true; + if (updateDisabled && AppConstants.MOZ_MAINTENANCE_SERVICE) { + document.getElementById("useService").hidden = true; + } + } else { + // Start with no option selected since we are still reading the value + document.getElementById("autoDesktop").removeAttribute("selected"); + document.getElementById("manualDesktop").removeAttribute("selected"); + // Start reading the correct value from the disk + this.updateReadPrefs(); + setEventListener( + "updateRadioGroup", + "command", + gMainPane.updateWritePrefs + ); + } + + if (AppConstants.platform == "win") { + // On Windows, the Application Update setting is an installation- + // specific preference, not a profile-specific one. Show a warning to + // inform users of this. + let updateContainer = document.getElementById( + "updateSettingsContainer" + ); + updateContainer.classList.add("updateSettingCrossUserWarningContainer"); + document.getElementById("updateSettingCrossUserWarning").hidden = false; + } + + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + // Check to see if the maintenance service is installed. + // If it isn't installed, don't show the preference at all. + let installed; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + wrk.open( + wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64 + ); + installed = wrk.readIntValue("Installed"); + wrk.close(); + } catch (e) {} + if (installed != 1) { + document.getElementById("useService").hidden = true; + } + } + } + + // 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); + + setEventListener("filter", "command", gMainPane.filter); + setEventListener("typeColumn", "click", gMainPane.sort); + setEventListener("actionColumn", "click", gMainPane.sort); + setEventListener("chooseFolder", "command", gMainPane.chooseFolder); + setEventListener("saveWhere", "command", gMainPane.handleSaveToCommand); + 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"), + ]); + + // 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("saveWhere"), + () => 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 } + ); + }); + }, + + // 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); + + const link = document.getElementById("browserContainersLearnMore"); + link.href = + Services.urlFormatter.formatURLPref("app.support.baseURL") + "containers"; + + 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; + } + 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"); + let warnOnQuitCheckbox = document.getElementById( + "browserRestoreSessionQuitWarning" + ); + if (pbAutoStartPref.value || startupPref.locked) { + checkbox.setAttribute("disabled", "true"); + warnOnQuitCheckbox.setAttribute("disabled", "true"); + } else { + checkbox.removeAttribute("disabled"); + } + newValue = pbAutoStartPref.value + ? false + : startupPref.value === this.STARTUP_PREF_RESTORE_SESSION; + if (checkbox.checked !== newValue) { + checkbox.checked = newValue; + let warnOnQuitPref = Preferences.get("browser.sessionstore.warnOnQuit"); + if (newValue && !warnOnQuitPref.locked && !pbAutoStartPref.value) { + warnOnQuitCheckbox.removeAttribute("disabled"); + } else { + warnOnQuitCheckbox.setAttribute("disabled", "true"); + } + } + }, + /** + * 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; + }, + + initBrowserLocale() { + // Enable telemetry. + Services.telemetry.setEventRecordingEnabled( + "intl.ui.browserLanguage", + true + ); + + // This will register the "command" listener. + let menulist = document.getElementById("defaultBrowserLanguage"); + new SelectionChangedMenulist(menulist, event => { + gMainPane.onBrowserLanguageChange(event); + }); + + gMainPane.setBrowserLocales(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. + */ + async setBrowserLocales(selected) { + let available = await getAvailableLocales(); + let localeNames = Services.intl.getLocaleDisplayNames(undefined, available); + 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 = "defaultBrowserLanguageSearch"; + menuitem.setAttribute( + "label", + await document.l10n.formatValue("browser-languages-search") + ); + menuitem.setAttribute("value", "search"); + fragment.appendChild(menuitem); + } + + let menulist = document.getElementById("defaultBrowserLanguage"); + 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.setAttribute("flex", "1"); + messageContainer.setAttribute("align", "center"); + + let description = document.createXULElement("description"); + description.classList.add("message-bar-description"); + 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.selectedLocales = 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. */ + onBrowserLanguageChange(event) { + let locale = event.target.value; + + if (locale == "search") { + gMainPane.showBrowserLanguages({ search: true }); + return; + } else if (locale == Services.locale.appLocaleAsBCP47) { + this.hideConfirmLanguageChangeMessageBar(); + return; + } + + // Note the change in telemetry. + gMainPane.recordBrowserLanguagesTelemetry("reorder"); + + let locales = Array.from( + new Set([locale, ...Services.locale.requestedLocales]).values() + ); + this.showConfirmLanguageChangeMessageBar(locales); + }, + + /** + * 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; + + let warnOnQuitCheckbox = document.getElementById( + "browserRestoreSessionQuitWarning" + ); + 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; + let warnOnQuitPref = Preferences.get("browser.sessionstore.warnOnQuit"); + if (!warnOnQuitPref.locked) { + warnOnQuitCheckbox.removeAttribute("disabled"); + } + } else { + newValue = this.STARTUP_PREF_HOMEPAGE; + warnOnQuitCheckbox.setAttribute("disabled", "true"); + } + 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.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"); + if (!shellSvc) { + defaultBrowserBox.hidden = true; + return; + } + let setDefaultPane = document.getElementById("setDefaultPane"); + let isDefault = shellSvc.isDefaultBrowser(false, true); + setDefaultPane.selectedIndex = isDefault ? 1 : 0; + 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) { + Cu.reportError(ex); + return; + } + + let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0; + document.getElementById("setDefaultPane").selectedIndex = selectedIndex; + } + }, + + /** + * 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 + ); + }, + + showBrowserLanguages({ 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 = { selected: gMainPane.selectedLocales, search, telemetryId }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml", + { closingCallback: this.browserLanguagesClosed }, + opts + ); + }, + + /* Show or hide the confirm change message bar based on the updated ordering. */ + browserLanguagesClosed() { + let { accepted, selected } = this.gBrowserLanguagesDialog; + let active = Services.locale.appLocalesAsBCP47; + + this.gBrowserLanguagesDialog.recordTelemetry( + accepted ? "accept" : "cancel" + ); + + // Prepare for changing the locales if they are different than the current locales. + if (selected && selected.join(",") != active.join(",")) { + gMainPane.showConfirmLanguageChangeMessageBar(selected); + gMainPane.setBrowserLocales(selected[0]); + return; + } + + // They matched, so we can reset the UI. + gMainPane.setBrowserLocales(Services.locale.appLocaleAsBCP47); + gMainPane.hideConfirmLanguageChangeMessageBar(); + }, + + 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, + ]); + 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. + */ + showTranslationExceptions() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/translation.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"); + }, + + updateMediaControlTelemetry() { + const telemetry = Services.telemetry.getHistogramById( + "MEDIA_CONTROL_SETTING_CHANGE" + ); + const checkbox = document.getElementById("mediaControlToggleEnabled"); + telemetry.add(checkbox.checked ? "EnableFromUI" : "DisableFromUI"); + }, + + /** + * 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(Cu.reportError); + }, + + /** + * 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.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; + } + }, + + /** + * Selects the correct item in the update radio group + */ + async updateReadPrefs() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + radiogroup.disabled = true; + try { + let enabled = await UpdateUtils.getAppUpdateAutoEnabled(); + radiogroup.value = enabled; + radiogroup.disabled = false; + } catch (error) { + Cu.reportError(error); + } + } + }, + + /** + * Writes the value of the update radio group to the disk + */ + async updateWritePrefs() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + let updateAutoValue = radiogroup.value == "true"; + radiogroup.disabled = true; + try { + await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue); + radiogroup.disabled = false; + } catch (error) { + Cu.reportError(error); + await this.updateReadPrefs(); + await this.reportUpdatePrefWriteError(error); + return; + } + + // If the value was changed to false the user should be given the option + // to discard an update if there is one. + if (!updateAutoValue) { + await this.checkUpdateInProgress(); + } + } + }, + + async reportUpdatePrefWriteError(error) { + let [title, message] = await document.l10n.formatValues([ + { id: "update-setting-write-failure-title" }, + { + id: "update-setting-write-failure-message", + args: { path: error.path }, + }, + ]); + + // Set up the Ok Button + let buttonFlags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK; + Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + null, + null, + null, + null, + {} + ); + }, + + async checkUpdateInProgress() { + let um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + if (!um.readyUpdate && !um.downloadingUpdate) { + return; + } + + let [ + title, + message, + okButton, + cancelButton, + ] = await document.l10n.formatValues([ + { id: "update-in-progress-title" }, + { id: "update-in-progress-message" }, + { id: "update-in-progress-ok-button" }, + { id: "update-in-progress-cancel-button" }, + ]); + + // Continue is the cancel button which is BUTTON_POS_1 and is set as the + // default so pressing escape or using a platform standard method of closing + // the UI will not discard the update. + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 + + Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; + + let rv = Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + okButton, + cancelButton, + null, + null, + {} + ); + if (rv != 1) { + let aus = Cc["@mozilla.org/updates/update-service;1"].getService( + Ci.nsIApplicationUpdateService + ); + aus.stopDownload(); + um.cleanupReadyUpdate(); + um.cleanupDownloadingUpdate(); + } + }, + + /** + * 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); + }, + + // 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 (aData != "true" && aData != "false") { + throw new Error("Invalid preference value for app.update.auto"); + } + document.getElementById("updateRadioGroup").value = aData; + } + }, + + // 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(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 { + 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) { + 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 (Cc["@mozilla.org/gio-service;1"]) { + let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService( + Ci.nsIGIOService + ); + var gioApps = gIOSvc.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 { + Cu.reportError("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(", "); + Cu.reportError( + 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) { + 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.getURLSpecFromFile(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.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. + * 3 - The default download location is elsewhere as specified by + * cloud storage API getDownloadFolder + * browser.download.downloadDir + * deprecated. + * browser.download.defaultFolder + * deprecated. + */ + + /** + * Enables/disables the folder field and Browse button based on whether a + * default download directory is being used. + */ + readUseDownloadDir() { + var downloadFolder = document.getElementById("downloadFolder"); + var chooseFolder = document.getElementById("chooseFolder"); + var useDownloadDirPreference = Preferences.get( + "browser.download.useDownloadDir" + ); + var dirPreference = Preferences.get("browser.download.dir"); + + downloadFolder.disabled = + !useDownloadDirPreference.value || dirPreference.locked; + chooseFolder.disabled = + !useDownloadDirPreference.value || dirPreference.locked; + + this.readCloudStorage().catch(Cu.reportError); + // don't override the preference's value in UI + return undefined; + }, + + /** + * Show/Hide the cloud storage radio button with provider name as label if + * cloud storage provider is in use. + * Select cloud storage radio button if browser.download.useDownloadDir is true + * and browser.download.folderList has value 3. Enables/disables the folder field + * and Browse button if cloud storage radio button is selected. + * + */ + async readCloudStorage() { + // Get preferred provider in use display name + let providerDisplayName = await CloudStorage.getProviderIfInUse(); + if (providerDisplayName) { + // Show cloud storage radio button with provider name in label + let saveToCloudRadio = document.getElementById("saveToCloud"); + document.l10n.setAttributes( + saveToCloudRadio, + "save-files-to-cloud-storage", + { + "service-name": providerDisplayName, + } + ); + saveToCloudRadio.hidden = false; + + let useDownloadDirPref = Preferences.get( + "browser.download.useDownloadDir" + ); + let folderListPref = Preferences.get("browser.download.folderList"); + + // Check if useDownloadDir is true and folderListPref is set to Cloud Storage value 3 + // before selecting cloudStorageradio button. Disable folder field and Browse button if + // 'Save to Cloud Storage Provider' radio option is selected + if (useDownloadDirPref.value && folderListPref.value === 3) { + document.getElementById("saveWhere").selectedItem = saveToCloudRadio; + document.getElementById("downloadFolder").disabled = true; + document.getElementById("chooseFolder").disabled = true; + } + } + }, + + /** + * Handle clicks to 'Save To <custom path> or <system default downloads>' and + * 'Save to <cloud storage provider>' if cloud storage radio button is displayed in UI. + * Sets browser.download.folderList value and Enables/disables the folder field and Browse + * button based on option selected. + */ + handleSaveToCommand(event) { + return this.handleSaveToCommandTask(event).catch(Cu.reportError); + }, + async handleSaveToCommandTask(event) { + if (event.target.id !== "saveToCloud" && event.target.id !== "saveTo") { + return; + } + // Check if Save To Cloud Storage Provider radio option is displayed in UI + // before continuing. + let saveToCloudRadio = document.getElementById("saveToCloud"); + if (!saveToCloudRadio.hidden) { + // When switching between SaveTo and SaveToCloud radio button + // with useDownloadDirPref value true, if selectedIndex is other than + // SaveTo radio button disable downloadFolder filefield and chooseFolder button + let saveWhere = document.getElementById("saveWhere"); + let useDownloadDirPref = Preferences.get( + "browser.download.useDownloadDir" + ); + if (useDownloadDirPref.value) { + let downloadFolder = document.getElementById("downloadFolder"); + let chooseFolder = document.getElementById("chooseFolder"); + downloadFolder.disabled = + saveWhere.selectedIndex || useDownloadDirPref.locked; + chooseFolder.disabled = + saveWhere.selectedIndex || useDownloadDirPref.locked; + } + + // Set folderListPref value depending on radio option + // selected. folderListPref should be set to 3 if Save To Cloud Storage Provider + // option is selected. If user switch back to 'Save To' custom path or system + // default Downloads, check pref 'browser.download.dir' before setting respective + // folderListPref value. If currentDirPref is unspecified folderList should + // default to 1 + let folderListPref = Preferences.get("browser.download.folderList"); + let saveTo = document.getElementById("saveTo"); + if (saveWhere.selectedItem == saveToCloudRadio) { + folderListPref.value = 3; + } else if (saveWhere.selectedItem == saveTo) { + let currentDirPref = Preferences.get("browser.download.dir"); + folderListPref.value = currentDirPref.value + ? await this._folderToIndex(currentDirPref.value) + : 1; + } + } + }, + + /** + * 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(Cu.reportError); + }, + 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(Cu.reportError); + + // don't override the preference's value in UI + return undefined; + }, + + async displayDownloadDirPrefTask() { + var folderListPref = Preferences.get("browser.download.folderList"); + var downloadFolder = document.getElementById("downloadFolder"); + var currentDirPref = Preferences.get("browser.download.dir"); + + // Used in defining the correct path to the folder icon. + var fph = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + var iconUrlSpec; + + let folderIndex = folderListPref.value; + if (folderIndex == 3) { + // When user has selected cloud storage, use value in currentDirPref to + // compute index to display download folder label and icon to avoid + // displaying blank downloadFolder label and icon on load of preferences UI + // Set folderIndex to 1 if currentDirPref is unspecified + folderIndex = currentDirPref.value + ? await this._folderToIndex(currentDirPref.value) + : 1; + } + + // Display a 'pretty' label or the path in the UI. + // note: downloadFolder.value is not read elsewhere in the code, its only purpose is to display to the user + if (folderIndex == 2) { + // Force the left-to-right direction when displaying a custom path. + downloadFolder.value = currentDirPref.value + ? `\u2066${currentDirPref.value.path}\u2069` + : ""; + iconUrlSpec = fph.getURLSpecFromFile(currentDirPref.value); + } else if (folderIndex == 1) { + // 'Downloads' + [downloadFolder.value] = await document.l10n.formatValues([ + { id: "downloads-folder-name" }, + ]); + iconUrlSpec = fph.getURLSpecFromFile(await this._indexToFolder(1)); + } else { + // 'Desktop' + [downloadFolder.value] = await document.l10n.formatValues([ + { id: "desktop-folder-name" }, + ]); + iconUrlSpec = fph.getURLSpecFromFile( + await this._getDownloadsFolder("Desktop") + ); + } + downloadFolder.style.backgroundImage = + "url(moz-icon://" + iconUrlSpec + "?size=16)"; + }, + + /** + * 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 flex="1" equalsize="always"> + <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> + </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) { + Cu.reportError("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 enabled() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } +} + +class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper { + constructor() { + super(TYPE_PDF, null); + } + + get enabled() { + return !Services.prefs.getBoolPref(PREF_PDFJS_DISABLED); + } +} + +class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper { + constructor(extension) { + super(null, extension); + } + + get enabled() { + return DownloadIntegration.shouldViewDownloadInternally(this.type); + } +} diff --git a/browser/components/preferences/moz.build b/browser/components/preferences/moz.build new file mode 100644 index 0000000000..67163934c7 --- /dev/null +++ b/browser/components/preferences/moz.build @@ -0,0 +1,20 @@ +# -*- 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 + +JAR_MANIFESTS += ["jar.mn"] + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Preferences") diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js new file mode 100644 index 0000000000..91e9e469ce --- /dev/null +++ b/browser/components/preferences/preferences.js @@ -0,0 +1,539 @@ +/* - This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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 findInPage.js */ +/* import-globals-from ../../base/content/utilityOverlay.js */ +/* import-globals-from ../../../toolkit/content/preferencesBindings.js */ + +"use strict"; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "AMTelemetry", + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "formAutofillParent", + "resource://formautofill/FormAutofillParent.jsm" +); + +XPCOMUtils.defineLazyGetter(this, "gSubDialog", function() { + const { SubDialogManager } = ChromeUtils.import( + "resource://gre/modules/SubDialog.jsm" + ); + 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: ({ title, frame }) => { + // Search within main document and highlight matched keyword. + gSearchResultsPane.searchWithinNode(title, gSearchResultsPane.query); + + // Search within sub-dialog document and highlight matched keyword. + gSearchResultsPane.searchWithinNode( + frame.contentDocument.firstElementChild, + gSearchResultsPane.query + ); + + // Creating tooltips for all the instances found + for (let node of gSearchResultsPane.listSearchTooltips) { + if (!node.tooltipNode) { + gSearchResultsPane.createSearchTooltip( + node, + gSearchResultsPane.query + ); + } + } + }, + }, + }); +}); + +var gLastCategory = { category: undefined, subcategory: undefined }; +const gXULDOMParser = new DOMParser(); + +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) { + gCategoryInits.set(categoryName, { + inited: false, + async init() { + 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(); + + // Asks Preferences to update the attribute value of the entire + // document again (this can be simplified if we could seperate the + // preferences of each pane.) + Preferences.updateAllElements(); + } + categoryObject.init(); + this.inited = true; + }, + }); +} + +document.addEventListener("DOMContentLoaded", init_all, { once: true }); + +function init_all() { + Preferences.forceEnableInstantApply(); + + 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); + } + // 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); + + gotoPref().then(() => { + let helpButton = document.getElementById("helpButton"); + let helpUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "preferences"; + helpButton.setAttribute("href", helpUrl); + + document.getElementById("addonsButton").addEventListener("click", e => { + if (e.button >= 2) { + // Ignore right clicks. + return; + } + let mainWindow = window.browsingContext.topChromeWindow; + mainWindow.BrowserOpenAddonsMgr(); + AMTelemetry.recordLinkEvent({ + object: "aboutPreferences", + value: "about:addons", + }); + }); + + 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(); +} + +async function gotoPref(aCategory) { + let categories = document.getElementById("categories"); + const kDefaultCategoryInternalName = "paneGeneral"; + const kDefaultCategory = "general"; + let hash = document.location.hash; + + let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName; + let breakIndex = category.indexOf("-"); + // Subcategories allow for selecting smaller sections of the preferences + // until proper search support is enabled (bug 1353954). + let subcategory = breakIndex != -1 && category.substring(breakIndex + 1); + if (subcategory) { + category = category.substring(0, breakIndex); + } + category = friendlyPrefCategoryNameToInternalName(category); + if (category != "paneSearchResults") { + gSearchResultsPane.query = null; + gSearchResultsPane.searchInput.value = ""; + gSearchResultsPane.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); + 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) { + Cu.reportError( + new Error( + "Error initializing preference category " + category + ": " + ex + ) + ); + throw ex; + } + + // Bail out of this goToPref if the category + // or subcategory changed during async operation. + if ( + gLastCategory.category !== category || + gLastCategory.subcategory !== subcategory + ) { + return; + } + + search(category, "data-category"); + + let mainContent = document.querySelector(".main-content"); + mainContent.scrollTop = 0; + + spotlight(subcategory, category); +} + +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(" ")); +} + +function maybeDisplayPoliciesNotice() { + if (Services.policies.status == Services.policies.ACTIVE) { + document.getElementById("policies-container").removeAttribute("hidden"); + } +} + +/** + * Filter the lastFallbackLocale from availableLocales if it doesn't have all + * of the needed strings. + * + * When the lastFallbackLocale isn't the defaultLocale, then by default only + * fluent strings are included. To fully use that locale you need the langpack + * to be installed, so if it isn't installed remove it from availableLocales. + */ +async function getAvailableLocales() { + let { availableLocales, defaultLocale, lastFallbackLocale } = Services.locale; + // If defaultLocale isn't lastFallbackLocale, then we still need the langpack + // for lastFallbackLocale for it to be useful. + if (defaultLocale != lastFallbackLocale) { + let lastFallbackId = `langpack-${lastFallbackLocale}@firefox.mozilla.org`; + let lastFallbackInstalled = await AddonManager.getAddonByID(lastFallbackId); + if (!lastFallbackInstalled) { + return availableLocales.filter(locale => locale != lastFallbackLocale); + } + } + return availableLocales; +} diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml new file mode 100644 index 0000000000..4b0c351c91 --- /dev/null +++ b/browser/components/preferences/preferences.xhtml @@ -0,0 +1,237 @@ +<?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: The 'oncommand' handler for 'focusSearch1' can not easily be rewritten (see Bug 371900) + hence we are allowing the inline handler in the script-src directive using the hash + sha512-X8+p/CqXeMdssOoFOf5RV+RpkvnN9pukQ20acGc7LqMgfYLW+lR0WAYT66OtSTpFHE/Qgx/ZCBs2RMc4QrA8FQ== + Additionally we should remove 'unsafe-inline' from style-src, see Bug 1579160 --> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; script-src chrome: 'sha512-X8+p/CqXeMdssOoFOf5RV+RpkvnN9pukQ20acGc7LqMgfYLW+lR0WAYT66OtSTpFHE/Qgx/ZCBs2RMc4QrA8FQ=='; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" /> + + <title data-l10n-id="pref-page-title"></title> + + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="browser/branding/brandings.ftl"/> + <link rel="localization" href="browser/branding/sync-brand.ftl"/> + <link rel="localization" href="browser/browser.ftl"/> + <link rel="localization" href="browser/preferences/preferences.ftl"/> + <!-- Used by fontbuilder.js --> + <link rel="localization" href="browser/preferences/fonts.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/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/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/aboutDialog.ftl"/> + <link rel="localization" href="browser/sanitize.ftl"/> + <link rel="localization" href="toolkit/updates/history.ftl"/> + <link rel="localization" href="security/certificates/deviceManager.ftl"/> + <link rel="localization" href="security/certificates/certManager.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"/> +</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-sync2" + data-l10n-attrs="tooltiptext" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1" data-l10n-id="pane-sync-title2"></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> + </richlistbox> + + <spacer flex="1"/> + + <hbox class="sidebar-footer-button" pack="center"> + <label id="addonsButton" is="text-link"> + <hbox align="center"> + <image class="sidebar-footer-icon addons-icon"/> + <label class="sidebar-footer-label" flex="1" data-l10n-id="addons-button-label"></label> + </hbox> + </label> + </hbox> + + <hbox class="sidebar-footer-button help-button" pack="center"> + <label id="helpButton" is="text-link"> + <hbox align="center"> + <image class="sidebar-footer-icon help-icon"/> + <label class="sidebar-footer-label" flex="1" data-l10n-id="help-button-label"></label> + </hbox> + </label> + </hbox> + </vbox> + + <keyset> + <!-- If you change the code within the oncommand handler of 'focusSearch1' you have to update the current hash of + sha512-X8+p/CqXeMdssOoFOf5RV+RpkvnN9pukQ20acGc7LqMgfYLW+lR0WAYT66OtSTpFHE/Qgx/ZCBs2RMc4QrA8FQ== within the CSP above. --> + <key data-l10n-id="focus-search" key="" modifiers="accel" id="focusSearch1" oncommand="gSearchResultsPane.searchInput.focus();"/> + </keyset> + + <vbox class="main-content" flex="1" align="start"> + <vbox class="pane-container"> + <hbox class="sticky-container" pack="end" align="start"> + <hbox id="policies-container" align="stretch" flex="1" hidden="true"> + <hbox align="center"> + <image class="info-icon"></image> + </hbox> + <hbox align="center" flex="1"> + <label class="policies-label" + flex="1" + href="about:policies" + useoriginprincipal="true" + is="text-link" + data-l10n-id="managed-notice"/> + </hbox> + </hbox> + <search-textbox + id="searchInput" + data-l10n-id="search-input-box" + data-l10n-attrs="placeholder, style" + hidden="true"/> + </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 + </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:body> +</html> diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml new file mode 100644 index 0000000000..09d66e136d --- /dev/null +++ b/browser/components/preferences/privacy.inc.xhtml @@ -0,0 +1,1025 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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"> + <label id="contentBlockingHeader"><html:h2 data-l10n-id="content-blocking-enhanced-tracking-protection"/></label> + <vbox data-subcategory="trackingprotection"> + <hbox align="start"> + <image id="trackingProtectionShield"/> + <vbox flex="1"> + <description class="description-with-side-element"> + <html:span id="contentBlockingDescription" class="tail-with-learn-more" data-l10n-id="content-blocking-section-top-level-description"></html:span> + <label id="contentBlockingLearnMore" class="learnMore" data-l10n-id="content-blocking-learn-more" is="text-link"/> + </description> + </vbox> + <vbox> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <button id="trackingProtectionExceptions" + is="highlightable-button" + class="accessory-button" + flex="1" + data-l10n-id="tracking-manage-exceptions" + preference="pref.privacy.disable_button.tracking_protection_exceptions" + search-l10n-ids=" + permissions-remove.label, + permissions-remove-all.label, + permissions-exceptions-etp-window.title, + permissions-exceptions-etp-desc, + "/> + </hbox> + </vbox> + </hbox> + <hbox id="fpiIncompatibilityWarning" align="start" hidden="true"> + <image class="info-icon fpi-warning-icon"></image> + <vbox flex="1"> + <description> + <html:span class="tail-with-learn-more" 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="content-blocking-category"> + <hbox> + <radio id="standardRadio" + value="standard" + data-l10n-id="enhanced-tracking-protection-setting-standard" + flex="1"/> + <button id="standardArrow" + is="highlightable-button" + class="arrowhead default-content-blocking-ui" + data-l10n-id="content-blocking-expand-section" + aria-expanded="false"/> + </hbox> + <vbox class="indent default-content-blocking-ui"> + <description data-l10n-id="content-blocking-etp-standard-desc"></description> + <vbox class="content-blocking-extra-information"> + <vbox class="indent"> + <hbox class="extra-information-label social-media-option" hidden="true"> + <image class="content-blocking-social-media-image"/> + <label data-l10n-id="content-blocking-social-media-trackers"/> + </hbox> + <hbox class="extra-information-label cross-site-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-cross-site-cookies"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-cross-site-tracking-cookies"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-plus-isolate-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-cross-site-tracking-cookies-plus-isolate"/> + </hbox> + <hbox class="extra-information-label pb-trackers-option" hidden="true"> + <image class="content-blocking-trackers-image"/> + <label data-l10n-id="content-blocking-private-windows"/> + </hbox> + <hbox class="extra-information-label trackers-option" hidden="true"> + <image class="content-blocking-trackers-image"/> + <label data-l10n-id="content-blocking-all-windows-tracking-content"/> + </hbox> + <hbox class="extra-information-label all-third-party-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-all-third-party-cookies"/> + </hbox> + <hbox class="extra-information-label all-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-all-cookies"/> + </hbox> + <hbox class="extra-information-label unvisited-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-unvisited-cookies"/> + </hbox> + <hbox class="extra-information-label cryptominers-option" hidden="true"> + <image class="content-blocking-cryptominers-image"/> + <label data-l10n-id="content-blocking-cryptominers"/> + </hbox> + <hbox class="extra-information-label fingerprinters-option" hidden="true"> + <image class="content-blocking-fingerprinters-image"/> + <label data-l10n-id="content-blocking-fingerprinters"/> + </hbox> + </vbox> + <html:div class="content-blocking-warning reload-tabs" hidden="true"> + <html:div class="content-blocking-reload-desc-container"> + <image class="content-blocking-info-image"/> + <html:span data-l10n-id="content-blocking-reload-description" + class="content-blocking-reload-description" /> + </html:div> + <button class="accessory-button reload-tabs-button" + is="highlightable-button" + data-l10n-id="content-blocking-reload-tabs-button"/> + </html:div> + </vbox> + </vbox> + </vbox> + <vbox id="contentBlockingOptionStrict" class="content-blocking-category"> + <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="content-blocking-extra-information"> + <vbox class="indent"> + <hbox class="extra-information-label social-media-option" hidden="true"> + <image class="content-blocking-social-media-image"/> + <label data-l10n-id="content-blocking-social-media-trackers"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-cross-site-tracking-cookies"/> + </hbox> + <hbox class="extra-information-label cross-site-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-cross-site-cookies"/> + </hbox> + <hbox class="extra-information-label pb-trackers-option" hidden="true"> + <image class="content-blocking-trackers-image"/> + <label data-l10n-id="content-blocking-private-windows"/> + </hbox> + <hbox class="extra-information-label trackers-option" hidden="true"> + <image class="content-blocking-trackers-image"/> + <label data-l10n-id="content-blocking-all-windows-tracking-content"/> + </hbox> + <hbox class="extra-information-label all-third-party-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-all-third-party-cookies"/> + </hbox> + <hbox class="extra-information-label all-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-all-cookies"/> + </hbox> + <hbox class="extra-information-label unvisited-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-unvisited-cookies"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-cross-site-tracking-cookies"/> + </hbox> + <hbox class="extra-information-label third-party-tracking-cookies-plus-isolate-option" hidden="true"> + <image class="content-blocking-cookies-image"/> + <label data-l10n-id="content-blocking-cross-site-tracking-cookies-plus-isolate"/> + </hbox> + <hbox class="extra-information-label cryptominers-option" hidden="true"> + <image class="content-blocking-cryptominers-image"/> + <label data-l10n-id="content-blocking-cryptominers"/> + </hbox> + <hbox class="extra-information-label fingerprinters-option" hidden="true"> + <image class="content-blocking-fingerprinters-image"/> + <label data-l10n-id="content-blocking-fingerprinters"/> + </hbox> + </vbox> + <html:div class="content-blocking-warning reload-tabs" hidden="true"> + <html:div class="content-blocking-reload-desc-container"> + <image class="content-blocking-info-image"/> + <html:span data-l10n-id="content-blocking-reload-description" + class="content-blocking-reload-description" /> + </html:div> + <button class="accessory-button reload-tabs-button" + is="highlightable-button" + data-l10n-id="content-blocking-reload-tabs-button"/> + </html:div> + <vbox class="content-blocking-warning"> + <vbox class="indent"> + <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"></html:span> + <label id="" class="learnMore contentBlockWarningLink" data-l10n-id="content-blocking-warning-learn-how" is="text-link"/> + </description> + </vbox> + </vbox> + </vbox> + </vbox> + </vbox> + <vbox id="contentBlockingOptionCustom" class="tracking-protection-ui content-blocking-category"> + <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="content-blocking-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> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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-and-social-media-trackers-plus-isolate" value="trackers-plus-isolate"/> + <menuitem data-l10n-id="sitedata-option-block-unvisited" value="unvisited"/> + <menuitem data-l10n-id="sitedata-option-block-all-third-party" value="all-third-parties"/> + <menuitem data-l10n-id="sitedata-option-block-all" value="always"/> + </menupopup> + </menulist> + </hbox> + </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-window.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 reload-tabs" hidden="true"> + <html:div class="content-blocking-reload-desc-container"> + <image class="content-blocking-info-image"/> + <html:span data-l10n-id="content-blocking-reload-description" + class="content-blocking-reload-description" /> + </html:div> + <button class="accessory-button reload-tabs-button" + is="highlightable-button" + data-l10n-id="content-blocking-reload-tabs-button"/> + </html:div> + <vbox class="content-blocking-warning"> + <vbox class="indent"> + <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"></html:span> + <label id="" class="learnMore contentBlockWarningLink" data-l10n-id="content-blocking-warning-learn-how" is="text-link"/> + </description> + </vbox> + </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><label + class="learnMore" is="text-link" href="https://www.mozilla.org/dnt" + data-l10n-id="do-not-track-learn-more"></label></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> + <label id="siteDataLearnMoreLink" + class="learnMore" is="text-link" data-l10n-id="sitedata-learn-more"/> + </description> + <hbox flex="1" id="deleteOnCloseNote" class="info-panel"> + <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" + preference="network.cookie.lifetimePolicy" + flex="1" /> + </hbox> + </vbox> + <vbox align="end"> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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"/> + </hbox> + <hbox> + <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, + "/> + </hbox> + <hbox> + <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, + " /> + </hbox> + </vbox> + </hbox> +</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> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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-window.title, + permissions-exceptions-saved-logins-desc, + "/> + </hbox> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox pack="end"> + <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"/> + </hbox> + </vbox> + </hbox> + <hbox> + <vbox flex="1"> + <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"/> + <label id="breachAlertsLearnMoreLink" class="learnMore" is="text-link" + data-l10n-id="forms-breach-alerts-learn-more-link"/> + </hbox> + </vbox> + </hbox> + </vbox> + <vbox> + <hbox id="masterPasswordRow" align="center"> + <checkbox id="useMasterPassword" + data-l10n-id="forms-primary-pw-use" + class="tail-with-learn-more"/> + <label id="primaryPasswordLearnMoreLink" class="learnMore" is="text-link" + flex="1" + data-l10n-id="forms-primary-pw-learn-more-link"/> + <spacer flex="1"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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> + </hbox> + <description class="indent tip-caption" + data-l10n-id="forms-primary-pw-former-name" + data-l10n-attrs="hidden" + flex="1"/> + </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"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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> + <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"/> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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-preferences.label, + item-offline-apps.label + "/> + </hbox> + </vbox> + </hbox> +</groupbox> + +<!-- Address Bar --> +<groupbox id="locationBarGroup" + data-category="panePrivacy" + hidden="true" + data-subcategory="locationBar"> + <label><html:h2 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" + preference="browser.urlbar.suggest.topsites"/> + <checkbox id="enginesSuggestion" data-l10n-id="addressbar-locbar-engines-option" + preference="browser.urlbar.suggest.engines"/> + <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-window.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-window.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-window.title, + permissions-site-microphone-desc, + permissions-site-microphone-disable-label, + permissions-site-microphone-disable-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"/> + <label id="notificationPermissionsLearnMore" + class="learnMore" + is="text-link" + data-l10n-id="permissions-notification-link"/> + </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-window.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-window.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-window.title, + permissions-site-xr-desc, + permissions-site-xr-disable-label, + permissions-site-xr-disable-desc, + " /> + </hbox> + </hbox> + </vbox> + + <separator flex="1"/> + + <hbox data-subcategory="permissions-block-popups"> + <checkbox id="popupPolicy" preference="dom.disable_open_during_load" + data-l10n-id="permissions-block-popups" + flex="1" /> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <button id="popupPolicyButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="permissions-block-popups-exceptions" + search-l10n-ids=" + permissions-address, + permissions-exceptions-popup-window.title, + permissions-exceptions-popup-desc, + " /> + </hbox> + </hbox> + + <hbox id="addonInstallBox"> + <checkbox id="warnAddonInstall" + data-l10n-id="permissions-addon-install-warning" + preference="xpinstall.whitelist.required" + flex="1" /> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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-window.title, + permissions-exceptions-addons-desc, + " /> + </hbox> + </hbox> + +</groupbox> + +<!-- Firefox Data Collection and Use --> +#ifdef MOZ_DATA_REPORTING +<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"/> + <label id="dataCollectionPrivacyNotice" + class="learnMore" is="text-link" + data-l10n-id="collection-privacy-notice"/> + </description> + <description> + <hbox id="telemetry-container" align="stretch" flex="1" hidden="true"> + <hbox align="top"> + <image class="info-icon-telemetry" flex="1"></image> + </hbox> + <hbox align="center" id="dataDescriptionBox" flex="1"> + <html:span id="telemetryDisabledDescription" class="tail-with-learn-more" data-l10n-id="collection-health-report-telemetry-disabled"/> + </hbox> + <hbox> + <button id="telemetryDataDeletionLearnMore" + class="learnMore" is="text-link" + data-l10n-id="collection-health-report-telemetry-disabled-link"/> + </hbox> + </hbox> + </description> + <vbox data-subcategory="reports"> + <description flex="1"> + <checkbox id="submitHealthReportBox" + data-l10n-id="collection-health-report" + class="tail-with-learn-more"/> + <label id="FHRLearnMore" + class="learnMore" is="text-link" + data-l10n-id="collection-health-report-link"/> + <vbox class="indent"> + <hbox align="center"> + <checkbox id="addonRecommendationEnabled" + class="tail-with-learn-more" + data-l10n-id="addon-recommendations"/> + <label id="addonRecommendationLearnMore" + class="learnMore" is="text-link" + data-l10n-id="addon-recommendations-link"/> + </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"/> + <label id="viewShieldStudies" + href="about:studies" + useoriginprincipal="true" + class="learnMore" is="text-link" + data-l10n-id="collection-studies-link"/> + </hbox> +#endif + +#ifdef MOZ_CRASHREPORTER + <hbox align="center"> + <checkbox id="automaticallySubmitCrashesBox" + class="tail-with-learn-more" + preference="browser.crashReports.unsubmittedCheck.autoSubmit2" + data-l10n-id="collection-backlogged-crash-reports" + flex="1"/> + <label id="crashReporterLearnMore" + class="learnMore" is="text-link" data-l10n-id="collection-backlogged-crash-reports-link"/> + </hbox> +#endif + </vbox> +</groupbox> +#endif + +<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"/> + <label id="enableSafeBrowsingLearnMore" + class="learnMore" is="text-link" data-l10n-id="security-enable-safe-browsing-link"/> + </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> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox pack="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 + "/> + </hbox> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox pack="end"> + <button id="viewSecurityDevicesButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="certs-devices" + preference="security.disable_button.openDeviceManager" + search-l10n-ids=" + devmgr.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 + "/> + </hbox> + </vbox> + </hbox> +</groupbox> + +<!-- HTTPS-ONLY Mode --> +<groupbox id="httpsOnlyBox" data-category="panePrivacy" hidden="true" hidehttpsonly="true"> + <label><html:h2 data-l10n-id="httpsonly-header"/></label> + <vbox data-subcategory="httpsonly" flex="1"> + <label id="httpsOnlyDescription" data-l10n-id="httpsonly-description"/> + <label id="httpsOnlyLearnMore" data-l10n-id="httpsonly-learn-more" class="learnMore" is="text-link"/> + </vbox> + <vbox> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <radiogroup 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> + </hbox> + </vbox> +</groupbox> + +</html:template> diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js new file mode 100644 index 0000000000..0706f9e8ba --- /dev/null +++ b/browser/components/preferences/privacy.js @@ -0,0 +1,2552 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 */ + +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "LoginHelper", + "resource://gre/modules/LoginHelper.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "OSKeyStore", + "resource://gre/modules/OSKeyStore.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "SiteDataManager", + "resource:///modules/SiteDataManager.jsm" +); +XPCOMUtils.defineLazyGetter(this, "L10n", () => { + return new Localization([ + "branding/brand.ftl", + "browser/preferences/preferences.ftl", + ]); +}); + +var { PrivateBrowsingUtils } = ChromeUtils.import( + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); + +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", +]; + +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"; +const PREF_PASSWORD_MANAGER_ENABLED = "signon.rememberSignons"; + +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.defineLazyServiceGetter( + this, + "listManager", + "@mozilla.org/url-classifier/listmanager;1", + "nsIUrlListManager" +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "OS_AUTH_ENABLED", + "signon.management.page.os-auth.enabled", + true +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gIsFirstPartyIsolated", + "privacy.firstparty.isolate", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gStatePartitioningMVPEnabled", + "browser.contentblocking.state-partitioning.mvp.ui.enabled", + false +); + +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" }, + + // 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.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" }, + + // 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.lifetimePolicy", 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" }, + // 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" }, + + // 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" }, +]); + +// 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" }, + ]); +} + +// 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() { + if (gStatePartitioningMVPEnabled) { + let warnings = document.querySelectorAll( + ".content-blocking-warning-description" + ); + for (let warning of warnings) { + document.l10n.setAttributes( + warning, + "content-blocking-and-isolating-etp-warning-description-2" + ); + } + document.getElementById( + "fpiIncompatibilityWarning" + ).hidden = !gIsFirstPartyIsolated; + } + + let links = document.querySelectorAll(".contentBlockWarningLink"); + let contentBlockingWarningUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "turn-off-etp-desktop"; + for (let link of links) { + link.setAttribute("href", contentBlockingWarningUrl); + } +} + +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); + } + }); + }, + + /** + * Initialize autocomplete to ensure prefs are in sync. + */ + _initAutocomplete() { + Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"].getService( + Ci.mozIPlacesAutoComplete + ); + }, + + 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"); + + if (httpsOnlyOnPref) { + httpsOnlyRadioGroup.value = "enabled"; + } else if (httpsOnlyOnPBMPref) { + httpsOnlyRadioGroup.value = "privateOnly"; + } else { + httpsOnlyRadioGroup.value = "disabled"; + } + }, + + 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() { + let exposeHttpsOnly = Services.prefs.getBoolPref( + "browser.preferences.exposeHTTPSOnly" + ); + let httpsOnlyBox = document.getElementById("httpsOnlyBox"); + + if (!exposeHttpsOnly) { + httpsOnlyBox.setAttribute("hidehttpsonly", "true"); + return; + } + + httpsOnlyBox.removeAttribute("hidehttpsonly"); + + let link = document.getElementById("httpsOnlyLearnMore"); + let httpsOnlyURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "https-only-prefs"; + link.setAttribute("href", httpsOnlyURL); + + // 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() + ); + }, + + /** + * 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.initializeHistoryMode(); + this.updateHistoryModePane(); + this.updatePrivacyMicroControls(); + this.initAutoStartPrivateBrowsingReverter(); + this._initAutocomplete(); + + /* 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("network.cookie.lifetimePolicy").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( + "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._initMasterPasswordUI(); + + this.initListenersForExtensionControllingPasswordManager(); + // set up the breach alerts Learn More link with the correct URL + const breachAlertsLearnMoreLink = document.getElementById( + "breachAlertsLearnMoreLink" + ); + const breachAlertsLearnMoreUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "lockwise-alerts"; + breachAlertsLearnMoreLink.setAttribute("href", breachAlertsLearnMoreUrl); + + 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 + ); + 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("deleteOnClose", () => this.readDeleteOnClose()); + setSyncToPrefListener("deleteOnClose", () => this.writeDeleteOnClose()); + 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); + } + } + + // When these prefs are made the default, add this data-l10n-id directly to privacy.inc.xhtml. + if ( + Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.newNewtabExperience.enabled" + ) || + Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.customizationMenu.enabled" + ) + ) { + document + .getElementById("topSitesSuggestion") + .setAttribute("data-l10n-id", "addressbar-locbar-shortcuts-option"); + } else { + document + .getElementById("topSitesSuggestion") + .setAttribute("data-l10n-id", "addressbar-locbar-topsites-option"); + } + + this.initSiteDataControls(); + setEventListener( + "clearSiteDataButton", + "command", + gPrivacyPane.clearSiteData + ); + setEventListener( + "siteDataSettings", + "command", + gPrivacyPane.showSiteDataSettings + ); + let url = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "storage-permissions"; + document.getElementById("siteDataLearnMoreLink").setAttribute("href", url); + + let notificationInfoURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + "push"; + document + .getElementById("notificationPermissionsLearnMore") + .setAttribute("href", notificationInfoURL); + + if (AppConstants.MOZ_DATA_REPORTING) { + this.initDataCollection(); + if (AppConstants.MOZ_CRASHREPORTER) { + this.initSubmitCrashes(); + } + this.initSubmitHealthReport(); + setEventListener( + "submitHealthReportBox", + "command", + gPrivacyPane.updateSubmitHealthReport + ); + setEventListener( + "telemetryDataDeletionLearnMore", + "command", + gPrivacyPane.showDataDeletion + ); + 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(); + + // 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(); + + let link = document.getElementById("contentBlockingLearnMore"); + let contentBlockingUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "enhanced-tracking-protection"; + link.setAttribute("href", contentBlockingUrl); + + // 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" + ); + let l10nID = gStatePartitioningMVPEnabled + ? "sitedata-option-block-cross-site-tracking-cookies-including-social-media" + : "sitedata-option-block-cross-site-and-social-media-trackers"; + document.l10n.setAttributes(contentBlockOptionSocialMedia, l10nID); + } + if (gStatePartitioningMVPEnabled) { + let contentBlockOptionIsolate = document.getElementById( + "isolateCookiesSocialMedia" + ); + document.l10n.setAttributes( + contentBlockOptionIsolate, + "sitedata-option-block-cross-site-cookies-including-social-media" + ); + } + + setUpContentBlockingWarnings(); + }, + + 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; + } + 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-option" + ).hidden = true; + document.querySelector( + selector + " .third-party-tracking-cookies-plus-isolate-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": + let cookieSelector = gStatePartitioningMVPEnabled + ? " .cross-site-cookies-option" + : " .third-party-tracking-cookies-plus-isolate-option"; + document.querySelector(selector + cookieSelector).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.cookieBehavior; + 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; + let cookieExpirationLocked = Services.prefs.prefIsLocked( + "network.cookie.lifetimePolicy" + ); + deleteOnCloseCheckbox.disabled = + privateBrowsing || completelyBlockCookies || cookieExpirationLocked; + 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" + ); + 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; + if (stpCookiePref.value) { + stpPref.value = true; + } + break; + case "private": + enabledPref.value = false; + pbmPref.value = true; + if (stpCookiePref.value) { + stpPref.value = false; + } + break; + case "never": + enabledPref.value = false; + pbmPref.value = false; + if (stpCookiePref.value) { + stpPref.value = false; + } + break; + } + }, + + toggleExpansion(e) { + let carat = e.target; + carat.classList.toggle("up"); + carat.closest(".content-blocking-category").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() { + // Check the "Delete cookies when Firefox is closed" checkbox and disable the setting + // when we're in auto private mode (or reset it back otherwise). + document.getElementById("deleteOnClose").checked = this.readDeleteOnClose(); + + 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.setAttribute("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"); + }, + }); + }, + + /** + * 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", + 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 + * network.cookie.lifetimePolicy + * - determines how long cookies are stored: + * 0 means keep cookies until they expire + * 2 means keep cookies until the browser is closed + */ + + readDeleteOnClose() { + let privateBrowsing = Preferences.get("browser.privatebrowsing.autostart") + .value; + if (privateBrowsing) { + return true; + } + + let lifetimePolicy = Preferences.get("network.cookie.lifetimePolicy").value; + return lifetimePolicy == Ci.nsICookieService.ACCEPT_SESSION; + }, + + writeDeleteOnClose() { + let checkbox = document.getElementById("deleteOnClose"); + return checkbox.checked + ? Ci.nsICookieService.ACCEPT_SESSION + : Ci.nsICookieService.ACCEPT_NORMALLY; + }, + + /** + * Reads the network.cookie.cookieBehavior preference value and + * enables/disables the "blockCookiesMenu" menulist accordingly. + */ + readBlockCookies() { + let bcControl = document.getElementById("blockCookiesMenu"); + bcControl.disabled = + Services.cookies.cookieBehavior == 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.cookieBehavior) { + 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 + ); + }, + + 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" + ); + }, + + // 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 + ); + }, + + // 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.isMasterPasswordSet(); + + 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")); + + let learnMoreLink = document.getElementById("primaryPasswordLearnMoreLink"); + let learnMoreURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "primary-password-stored-logins"; + learnMoreLink.setAttribute("href", learnMoreURL); + }, + + /** + * 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 master password may be changed. + */ + async changeMasterPassword() { + // Require OS authentication before the user can set a Master Password. + // OS reauthenticate functionality is not available on Linux yet (bug 1527745) + if ( + !LoginHelper.isMasterPasswordSet() && + OS_AUTH_ENABLED && + OSKeyStore.canReauth() + ) { + let messageId = + "primary-password-os-auth-dialog-message-" + AppConstants.platform; + let [messageText, captionText] = await 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; + }, + + /** + * 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; + + // 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; + + // 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" + ); + + let learnMoreLink = document.getElementById("enableSafeBrowsingLearnMore"); + let phishingUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "phishing-malware"; + learnMoreLink.setAttribute("href", phishingUrl); + + enableSafeBrowsing.addEventListener("command", function() { + safeBrowsingPhishingPref.value = enableSafeBrowsing.checked; + safeBrowsingMalwarePref.value = enableSafeBrowsing.checked; + + if (enableSafeBrowsing.checked) { + blockDownloads.removeAttribute("disabled"); + if (blockDownloads.checked) { + blockUncommonUnwanted.removeAttribute("disabled"); + } + } else { + blockDownloads.setAttribute("disabled", "true"); + blockUncommonUnwanted.setAttribute("disabled", "true"); + } + }); + + blockDownloads.addEventListener("command", function() { + blockDownloadsPref.value = blockDownloads.checked; + if (blockDownloads.checked) { + blockUncommonUnwanted.removeAttribute("disabled"); + } else { + blockUncommonUnwanted.setAttribute("disabled", "true"); + } + }); + + 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; + }, + + /** + * 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() { + this._setupLearnMoreLink( + "toolkit.datacollection.infoURL", + "dataCollectionPrivacyNotice" + ); + }, + + initSubmitCrashes() { + this._setupLearnMoreLink( + "toolkit.crashreporter.infoURL", + "crashReporterLearnMore" + ); + }, + + /** + * Set up or hide the Learn More links for various data collection options + */ + _setupLearnMoreLink(pref, element) { + // set up the Learn More link with the correct URL + let url = Services.urlFormatter.formatURLPref(pref); + let el = document.getElementById(element); + + if (url) { + el.setAttribute("href", url); + } else { + el.setAttribute("hidden", "true"); + } + }, + + /** + * Initialize the health report service reference and checkbox. + */ + initSubmitHealthReport() { + this._setupLearnMoreLink( + "datareporting.healthreport.infoURL", + "FHRLearnMore" + ); + + let checkbox = document.getElementById("submitHealthReportBox"); + + // Telemetry is only sending data if MOZ_TELEMETRY_REPORTING is defined. + // We still want to display the preferences panel if that's not the case, but + // we want it to be disabled and unchecked. + if ( + Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED) || + !AppConstants.MOZ_TELEMETRY_REPORTING + ) { + checkbox.setAttribute("disabled", "true"); + return; + } + + checkbox.checked = + Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED) && + AppConstants.MOZ_TELEMETRY_REPORTING; + }, + + /** + * Update the health report preference with state from checkbox. + */ + updateSubmitHealthReport() { + let checkbox = document.getElementById("submitHealthReportBox"); + 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 learn more link. + const url = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "personalized-addons"; + document + .getElementById("addonRecommendationLearnMore") + .setAttribute("href", url); + + // 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; + } + }, +}; diff --git a/browser/components/preferences/search.inc.xhtml b/browser/components/preferences/search.inc.xhtml new file mode 100644 index 0000000000..1269003f67 --- /dev/null +++ b/browser/components/preferences/search.inc.xhtml @@ -0,0 +1,122 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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"> + <radio id="searchBarHiddenRadio" value="false" data-l10n-id="search-bar-hidden"/> + <image class="searchBarImage searchBarHiddenImage" role="presentation"/> + <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> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <menulist id="defaultEngine"> + <menupopup/> + </menulist> + </hbox> + </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> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <menulist id="defaultPrivateEngine"> + <menupopup/> + </menulist> + </hbox> + </hbox> + </vbox> + </groupbox> + + <groupbox id="searchSuggestionsGroup" data-category="paneSearch" hidden="true"> + <label><html:h2 data-l10n-id="search-suggestions-header" /></label> + <description 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"/> + <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-generic"/> + </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" + /> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <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> + <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..4cf45263e7 --- /dev/null +++ b/browser/components/preferences/search.js @@ -0,0 +1,1072 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ExtensionSettingsStore", + "resource://gre/modules/ExtensionSettingsStore.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "UrlbarPrefs", + "resource:///modules/UrlbarPrefs.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "UrlbarUtils", + "resource:///modules/UrlbarUtils.jsm" +); + +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.matchBuckets", type: "string" }, + { 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 = { + /** + * Initialize autocomplete to ensure prefs are in sync. + */ + _initAutocomplete() { + Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"].getService( + Ci.mozIPlacesAutoComplete + ); + }, + + 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"); + let searchEnginesURL = Services.wm.getMostRecentWindow( + "navigator:browser" + ).BrowserSearch.searchEnginesURL; + addEnginesLink.setAttribute("href", 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"); + window.addEventListener("unload", () => { + Services.obs.removeObserver(this, "browser-search-engine-modified"); + }); + + this._initAutocomplete(); + + 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._initShowSearchSuggestionsFirst(); + 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); + }, + + _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; + }, + + _initShowSearchSuggestionsFirst() { + this._urlbarSuggestionsPosPref = Preferences.get( + "browser.urlbar.matchBuckets" + ); + let checkbox = document.getElementById( + "showSearchSuggestionsFirstCheckbox" + ); + + this._urlbarSuggestionsPosPref.on("change", () => { + this._syncFromShowSearchSuggestionsFirstPref(checkbox); + }); + this._syncFromShowSearchSuggestionsFirstPref(checkbox); + + checkbox.addEventListener("command", () => { + this._syncToShowSearchSuggestionsFirstPref(checkbox.checked); + }); + }, + + _syncFromShowSearchSuggestionsFirstPref(checkbox) { + if (!this._urlbarSuggestionsPosPref.value) { + // The pref is cleared, meaning search suggestions are shown first. + checkbox.checked = true; + return; + } + // The pref has a value. If the first bucket in the pref is search + // suggestions, then check the checkbox. + let buckets = PlacesUtils.convertMatchBucketsStringToArray( + this._urlbarSuggestionsPosPref.value + ); + checkbox.checked = buckets[0] && buckets[0][0] == "suggestion"; + }, + + _syncToShowSearchSuggestionsFirstPref(checked) { + if (checked) { + // Show search suggestions first, so clear the pref since that's the + // default. + this._urlbarSuggestionsPosPref.reset(); + return; + } + // Show history first. + this._urlbarSuggestionsPosPref.value = "general:5,suggestion:Infinity"; + }, + + _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; + this._syncFromShowSearchSuggestionsFirstPref(positionCheckbox); + } 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; + } + }, + + observe(aEngine, aTopic, aVerb) { + if (aTopic == "browser-search-engine-modified") { + aEngine.QueryInterface(Ci.nsISearchEngine); + switch (aVerb) { + case "engine-added": + gEngineView._engineStore.addEngine(aEngine); + gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1); + gSearchPane.buildDefaultEngineDropDowns(); + break; + case "engine-changed": + gEngineView._engineStore.reloadIcons(); + // Only bother invalidating if the tree is valid. It might not be + // if we're here because we saved an engine keyword change when + // the input got blurred as a result of changing categories, which + // destroys the tree. + if (gEngineView.tree) { + gEngineView.invalidate(); + } + break; + case "engine-removed": + gSearchPane.remove(aEngine); + 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 != aEngine.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 != aEngine.name) { + gSearchPane.buildDefaultEngineDropDowns(); + } + } + 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); + 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 + ); + 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 + ); + }, +}; + +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; + return 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 ["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 == this.originalEngine; + }, + + addEngine(aEngine) { + this._engines.push(this._cloneEngine(aEngine)); + }, + + 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++; + } + } + Services.search.resetToOriginalDefaultEngine(); + 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 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(); + 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(); + }); +} + +EngineView.prototype = { + _engineStore: null, + tree: null, + + 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() { + // We don't allow the last remaining engine to be removed, thus the + // `this.lastEngineIndex != 0` check. + return ( + this.selectedIndex != -1 && + this.lastEngineIndex != 0 && + !this._getLocalShortcut(this.selectedIndex) + ); + }, + + /** + * 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..f9eeaf0088 --- /dev/null +++ b/browser/components/preferences/searchResults.inc.xhtml @@ -0,0 +1,28 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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-message"> + <html:span data-l10n-name="query" id="sorry-message-query"/> + </label> + <label id="need-help" data-l10n-id="search-results-help-link"> + <html:a class="text-link" data-l10n-name="url" target="_blank"></html:a> + </label> + </vbox> + <vbox class="no-results-container no-results-image-container" align="center"> + <image class="no-results-image"></image> + </vbox> +</groupbox> diff --git a/browser/components/preferences/sync.inc.xhtml b/browser/components/preferences/sync.inc.xhtml new file mode 100644 index 0000000000..310e83e8ba --- /dev/null +++ b/browser/components/preferences/sync.inc.xhtml @@ -0,0 +1,244 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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-title2"/> +</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-description"/> + </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-signin2"/> + </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-title2"/></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" + 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 sync-not-configured"> + <vbox flex="1"> + <label data-l10n-id="prefs-sync-offer-setup-label"/> + </vbox> + <vbox> + <button id="syncSetup" + is="highlightable-button" + class="accessory-button needs-account-ready" + data-l10n-id="prefs-sync-setup"/> + </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 sync-configured"> + <label data-l10n-id="sync-currently-syncing-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-prefs"/> + </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"> + <label id="connect-another-device" + is="text-link" + class="fxaMobilePromo" + 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..1bbff20c82 --- /dev/null +++ b/browser/components/preferences/sync.js @@ -0,0 +1,608 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { Weave } = ChromeUtils.import("resource://services-sync/main.js"); +var { FxAccounts, fxAccounts } = ChromeUtils.import( + "resource://gre/modules/FxAccounts.jsm" +); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() { + return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js", {}); +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + UIState: "resource://services-sync/UIState.jsm", +}); + +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", + undefined + ); + 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); + }); + + XPCOMUtils.defineLazyGetter(this, "_accountsStringBundle", () => { + return Services.strings.createBundle( + "chrome://browser/locale/accounts.properties" + ); + }); + + 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"); + + // document.location.search is empty, so we simply match on `action=pair`. + if ( + location.href.includes("action=pair") && + location.hash == "#sync" && + UIState.get().status == UIState.STATUS_SIGNED_IN + ) { + gSyncPane.pairAnotherDevice(); + } + }, + + _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("verifiedManage", "keypress", function(event) { + gSyncPane.openManageFirefoxAccount(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(url, { + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( + {} + ), + }); + }, + + async signIn() { + 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. + 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(); + } + }, + + openManageFirefoxAccount(event) { + if (this.clickOrSpaceOrEnterPressed(event)) { + this.manageFirefoxAccount(); + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + }, + + manageFirefoxAccount() { + FxAccounts.config.promiseManageURI(this._getEntryPoint()).then(url => { + this.openContentInBrowser(url, { + replaceQueryString: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + }); + }, + + verifyFirefoxAccount() { + let showVerifyNotification = data => { + let isError = !data; + let maybeNot = isError ? "Not" : ""; + let sb = this._accountsStringBundle; + let title = sb.GetStringFromName("verification" + maybeNot + "SentTitle"); + let email = !isError && data ? data.email : ""; + let body = sb.formatStringFromName( + "verification" + maybeNot + "SentBody", + [email] + ); + new Notification(title, { body }); + }; + + let onError = () => { + showVerifyNotification(); + }; + + let onSuccess = data => { + if (data) { + showVerifyNotification(data); + } else { + onError(); + } + }; + + fxAccounts + .resendVerificationEmail() + .then(() => fxAccounts.getSignedInUser(), onError) + .then(onSuccess, onError); + }, + + // 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) => { + let enabled = Services.prefs.getBoolPref(prefName, false); + if (enabled) { + elt.removeAttribute("hidden"); + } else { + elt.setAttribute("hidden", "true"); + } + }; + + 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/.eslintrc.js b/browser/components/preferences/tests/.eslintrc.js new file mode 100644 index 0000000000..1779fd7f1c --- /dev/null +++ b/browser/components/preferences/tests/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; 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..304275c69a --- /dev/null +++ b/browser/components/preferences/tests/browser.ini @@ -0,0 +1,124 @@ +[DEFAULT] +prefs = + extensions.formautofill.available='on' + extensions.formautofill.creditCards.available=true + 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_applications_selection.js] +[browser_advanced_update.js] +skip-if = !updater +[browser_basic_rebuild_fonts_test.js] +[browser_bug410900.js] +[browser_bug731866.js] +[browser_bug1579418.js] +[browser_experimental_features.js] +[browser_experimental_features_filter.js] +[browser_experimental_features_hidden_when_not_public.js] +[browser_experimental_features_resetall.js] +[browser_filetype_dialog.js] +[browser_search_no_results_change_category.js] +[browser_search_within_preferences_1.js] +skip-if = (os == 'win' && (processor == "x86_64" || processor == "aarch64")) # Bug 1480314, aarch64 due to 1536560 +[browser_search_within_preferences_2.js] +[browser_search_within_preferences_command.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_bug795764_cachedisabled.js] +[browser_bug1018066_resetScrollPosition.js] +[browser_bug1020245_openPreferences_to_paneContent.js] +[browser_bug1184989_prevent_scrolling_when_preferences_flipped.js] +skip-if = os == "mac" # 1664576 +support-files = + browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml +[browser_bug1547020_lockedDownloadDir.js] +[browser_cookie_exceptions_addRemove.js] +[browser_cert_export.js] +[browser_engines.js] +[browser_change_app_handler.js] +skip-if = os != "win" # Windows-specific handler application selection dialog +[browser_checkspelling.js] +[browser_cloud_storage.js] +[browser_connection.js] +[browser_connection_bug388287.js] +[browser_connection_bug1445991.js] +[browser_connection_bug1505330.js] +skip-if = (verify && debug && (os == 'linux' || os == 'mac')) +[browser_connection_dnsoverhttps.js] +[browser_contentblocking_categories.js] +[browser_contentblocking.js] +[browser_cookies_exceptions.js] +[browser_defaultbrowser_alwayscheck.js] +[browser_healthreport.js] +skip-if = true || !healthreport # Bug 1185403 for the "true" +[browser_homepages_filter_aboutpreferences.js] +[browser_homepages_use_bookmark.js] +[browser_homepage_default.js] +[browser_https_only_section.js] +[browser_extension_controlled.js] +skip-if = tsan || ccov && (os == 'linux' || os == 'win') # Linux: bug 1613530, Windows: bug 1437051 +[browser_languages_subdialog.js] +[browser_browser_languages_subdialog.js] +skip-if = tsan || (!debug && os == 'win') # Bug 1518370 +[browser_layersacceleration.js] +[browser_localSearchShortcuts.js] +[browser_masterpassword.js] +[browser_media_control.js] +skip-if = (os == 'win' && os_version == '6.1') || (verify && os == 'mac') #Bug 1667454 +[browser_newtab_menu.js] +[browser_notifications_do_not_disturb.js] +[browser_password_management.js] +[browser_performance.js] +skip-if = !e10s +[browser_performance_e10srollout.js] +skip-if = !e10s +[browser_performance_non_e10s.js] +skip-if = e10s +[browser_permissions_checkPermissionsWereAdded.js] +[browser_permissions_urlFieldHidden.js] +[browser_proxy_backup.js] +[browser_privacypane_2.js] +[browser_privacypane_3.js] +[browser_privacy_passwordGenerationAndAutofill.js] +[browser_sanitizeOnShutdown_prefLocked.js] +[browser_searchDefaultEngine.js] +support-files = + engine1/manifest.json + engine2/manifest.json +[browser_searchRestoreDefaults.js] +[browser_searchShowSuggestionsFirst.js] +[browser_searchsuggestions.js] +[browser_security-1.js] +[browser_security-2.js] +[browser_spotlight.js] +[browser_site_login_exceptions.js] +[browser_permissions_dialog.js] +[browser_statePartitioning_strings.js] +[browser_subdialogs.js] +support-files = + subdialog.xhtml + subdialog2.xhtml +[browser_sync_disabled.js] +[browser_sync_pairing.js] +[browser_telemetry.js] +# Skip this test on Android as FHR and Telemetry are separate systems there. +skip-if = !telemetry || (os == 'linux' && debug) +[browser_warning_permanent_private_browsing.js] +[browser_containers_name_input.js] +run-if = nightly_build # Containers is enabled only on Nightly +[browser_fluent.js] +[browser_hometab_restore_defaults.js] +skip-if = debug #Bug 1517966 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..b78242bfe4 --- /dev/null +++ b/browser/components/preferences/tests/browser_advanced_update.js @@ -0,0 +1,190 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cm = Components.manager; + +const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService( + Ci.nsIUUIDGenerator +); + +const mockUpdateManager = { + contractId: "@mozilla.org/updates/update-manager;1", + + _mockClassId: uuidGenerator.generateUUID(), + + _originalClassId: "", + + QueryInterface: ChromeUtils.generateQI(["nsIUpdateManager"]), + + createInstance(outer, iiD) { + if (outer) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + 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_applications_selection.js b/browser/components/preferences/tests/browser_applications_selection.js new file mode 100644 index 0000000000..2adf10b2b5 --- /dev/null +++ b/browser/components/preferences/tests/browser_applications_selection.js @@ -0,0 +1,408 @@ +SimpleTest.requestCompleteLog(); +ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm", + this +); + +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_task(async function setup() { + // 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(); + } + item.click(); + popup.hidePopup(); + 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 always ask 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..4769c84910 --- /dev/null +++ b/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js @@ -0,0 +1,236 @@ +Services.prefs.setBoolPref("browser.preferences.instantApply", true); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.preferences.instantApply"); +}); + +add_task(async function() { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + await gBrowser.contentWindow.gMainPane._selectDefaultLanguageGroupPromise; + 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, + 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..bae546a9b1 --- /dev/null +++ b/browser/components/preferences/tests/browser_browser_languages_subdialog.js @@ -0,0 +1,785 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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", + }, + }, + applications: { + gecko: { + strict_min_version: AppConstants.MOZ_APP_VERSION, + id: langpackId(locale), + 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("defaultBrowserLanguageSearch").doCommand(); + doc.getElementById("defaultBrowserLanguage").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.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"); + + // 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.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; + is(messageBar.hidden, false, "The message bar is now visible"); + 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( + firstDialogId < 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.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); + + // 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.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"); + + // 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; + + // 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], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let defaultMenulist = doc.getElementById("defaultBrowserLanguage"); + 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], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let defaultMenulist = doc.getElementById("defaultBrowserLanguage"); + 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.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("defaultBrowserLanguage"); + 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[availableLocales.findIndex(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"]]); +}); 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..4e8807a0c4 --- /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..1aab231f22 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Services.prefs.setBoolPref("browser.preferences.instantApply", true); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.preferences.instantApply"); +}); + +// 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 + 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..1f9bc5c89d --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js @@ -0,0 +1,105 @@ +const { E10SUtils } = ChromeUtils.import( + "resource://gre/modules/E10SUtils.jsm" +); +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(); + EventUtils.sendString(" "); + await checkPageScrolling(container, "button"); + + // Test checkbox + let checkbox = doc.getElementById("checkbox"); + checkbox.focus(); + EventUtils.sendString(" "); + ok(checkbox.checked, "Checkbox is checked"); + await checkPageScrolling(container, "checkbox"); + + // Test radio + let radiogroup = doc.getElementById("radiogroup"); + radiogroup.focus(); + EventUtils.sendString(" "); + await checkPageScrolling(container, "radio"); + }); + + 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(); + EventUtils.sendString(" "); + is( + engineList.view.selection.currentIndex, + 0, + "Search engineList is selected" + ); + EventUtils.sendString(" "); + await checkPageScrolling(container, "search engineList"); + } + ); + + // 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(); + EventUtils.sendString(" "); + await checkPageScrolling(doc.documentElement, "session restore"); + + gBrowser.removeCurrentTab(); + finish(); +}); + +function checkPageScrolling(container, type) { + return new Promise(resolve => { + setTimeout(() => { + is( + container.scrollTop, + 0, + "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..b9a6b8a1ff --- /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..7dd89ba214 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1579418.js @@ -0,0 +1,48 @@ +/* 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")); + + // HOME_MODE_BLANK + homeMode.value = 1; + + homeMode.dispatchEvent(new Event("command")); + + await TestUtils.waitForCondition( + () => customSettings.hidden === true, + "Wait for customSettings to be hidden." + ); + + // 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..c1eba138f5 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug731866.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const browserContainersGroupDisabled = !SpecialPowers.getBoolPref( + "privacy.userContext.ui.enabled" +); + +const httpsOnlyVisible = SpecialPowers.getBoolPref( + "browser.preferences.exposeHTTPSOnly" +); + +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; + } + + // HTTPS-Only Mode is exposed depending on the preference. + if (element.id == "httpsOnlyBox" && !httpsOnlyVisible) { + is_element_hidden(element, "Disabled httpsOnlyBox 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..c454454df6 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug795764_cachedisabled.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const httpsOnlyVisible = SpecialPowers.getBoolPref( + "browser.preferences.exposeHTTPSOnly" +); + +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"); + if (attributeValue == "panePrivacy") { + // HTTPS-Only Mode is exposed depending on the preference. + if (element.id == "httpsOnlyBox") { + if (httpsOnlyVisible) { + is_element_visible(element, "HTTPSOnly should be visible"); + } else { + is_element_hidden(element, "HTTPSOnly should not be visible"); + } + continue; + } + + 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..0c242728d2 --- /dev/null +++ b/browser/components/preferences/tests/browser_cert_export.js @@ -0,0 +1,156 @@ +/* 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 = await OS.File.read(destFile.path, { encoding }); + 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..850f0b9c35 --- /dev/null +++ b/browser/components/preferences/tests/browser_change_app_handler.js @@ -0,0 +1,153 @@ +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, + 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, + 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..859e539fb1 --- /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_cloud_storage.js b/browser/components/preferences/tests/browser_cloud_storage.js new file mode 100644 index 0000000000..1870942312 --- /dev/null +++ b/browser/components/preferences/tests/browser_cloud_storage.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); +ChromeUtils.defineModuleGetter( + this, + "CloudStorage", + "resource://gre/modules/CloudStorage.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "FileUtils", + "resource://gre/modules/FileUtils.jsm" +); + +const DROPBOX_DOWNLOAD_FOLDER = "Dropbox"; +const CLOUD_SERVICES_PREF = "cloud.services."; + +function create_subdir(dir, subdirname) { + let subdir = dir.clone(); + subdir.append(subdirname); + if (subdir.exists()) { + subdir.remove(true); + } + subdir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return subdir; +} + +/** + * Replaces a directory service entry with a given nsIFile. + */ +function registerFakePath(key, folderName) { + let dirsvc = Services.dirsvc.QueryInterface(Ci.nsIProperties); + + // Create a directory inside the profile and register it as key + let profD = dirsvc.get("ProfD", Ci.nsIFile); + // create a subdir just to keep our files out of the way + let file = create_subdir(profD, folderName); + + let originalFile; + try { + // If a file is already provided save it and undefine, otherwise set will + // throw for persistent entries (ones that are cached). + originalFile = dirsvc.get(key, Ci.nsIFile); + dirsvc.undefine(key); + } catch (e) { + // dirsvc.get will throw if nothing provides for the key and dirsvc.undefine + // will throw if it's not a persistent entry, in either case we don't want + // to set the original file in cleanup. + originalFile = undefined; + } + + dirsvc.set(key, file); + registerCleanupFunction(() => { + dirsvc.undefine(key); + if (originalFile) { + dirsvc.set(key, originalFile); + } + }); +} + +async function mock_dropbox() { + // Mock Dropbox Download folder in Home directory + let downloadFolder = FileUtils.getFile("Home", [ + DROPBOX_DOWNLOAD_FOLDER, + "Downloads", + ]); + if (!downloadFolder.exists()) { + downloadFolder.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } + console.log(downloadFolder.path); + + registerCleanupFunction(() => { + if (downloadFolder.exists()) { + downloadFolder.remove(true); + } + }); +} + +add_task(async function setup() { + // Create mock Dropbox download folder for cloudstorage API + // Set prefs required to display second radio option + // 'Save to Dropbox' under Downloads + let folderName = "CloudStorage"; + registerFakePath("Home", folderName); + await mock_dropbox(); + await SpecialPowers.pushPrefEnv({ + set: [ + [CLOUD_SERVICES_PREF + "api.enabled", true], + [CLOUD_SERVICES_PREF + "storage.key", "Dropbox"], + ], + }); +}); + +add_task(async function test_initProvider() { + // Get preferred provider key + let preferredProvider = await CloudStorage.getPreferredProvider(); + is(preferredProvider, "Dropbox", "Cloud Storage preferred provider key"); + + let isInitialized = await CloudStorage.init(); + is(isInitialized, true, "Providers Metadata successfully initialized"); + + // Get preferred provider in use display name + let providerDisplayName = await CloudStorage.getProviderIfInUse(); + is( + providerDisplayName, + "Dropbox", + "Cloud Storage preferred provider display name" + ); +}); + +add_task(async function() { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let saveWhereOptions = doc.getElementById("saveWhere"); + let saveToCloud = doc.getElementById("saveToCloud"); + is(saveWhereOptions.itemCount, 3, "Radio options count"); + is_element_visible(saveToCloud, "Save to Dropbox option is visible"); + + let saveTo = doc.getElementById("saveTo"); + ok(saveTo.selected, "Ensure first option is selected by default"); + is( + Services.prefs.getIntPref("browser.download.folderList"), + 1, + "Set to system downloadsfolder as the default download location" + ); + + let downloadFolder = doc.getElementById("downloadFolder"); + let chooseFolder = doc.getElementById("chooseFolder"); + is(downloadFolder.disabled, false, "downloadFolder filefield is enabled"); + is(chooseFolder.disabled, false, "chooseFolder button is enabled"); + + // Test click of second radio option sets browser.download.folderList as 3 + // which means the default download location is elsewhere as specified by + // cloud storage API getDownloadFolder and pref cloud.services.storage.key + saveToCloud.click(); + is( + Services.prefs.getIntPref("browser.download.folderList"), + 3, + "Default download location is elsewhere as specified by cloud storage API" + ); + is(downloadFolder.disabled, true, "downloadFolder filefield is disabled"); + is(chooseFolder.disabled, true, "chooseFolder button is disabled"); + + // Test selecting first radio option enables downloadFolder filefield and chooseFolder button + saveTo.click(); + is(downloadFolder.disabled, false, "downloadFolder filefield is enabled"); + is(chooseFolder.disabled, false, "chooseFolder button is enabled"); + + // Test selecting third radio option keeps downloadFolder and chooseFolder elements disabled + let alwaysAsk = doc.getElementById("alwaysAsk"); + saveToCloud.click(); + alwaysAsk.click(); + is(downloadFolder.disabled, true, "downloadFolder filefield is disabled"); + is(chooseFolder.disabled, true, "chooseFolder button is disabled"); + saveTo.click(); + ok(saveTo.selected, "Reset back first option as selected by default"); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_connection.js b/browser/components/preferences/tests/browser_connection.js new file mode 100644 index 0000000000..23e7ee0409 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection.js @@ -0,0 +1,144 @@ +/* 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/. */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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"); + Services.prefs.clearUserPref("browser.preferences.instantApply"); + }); + + 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..76d5090b63 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_bug388287.js @@ -0,0 +1,130 @@ +/* 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/ */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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", "ftp", "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" + ); + } + }); + + /* + 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, ftpPref, ftpPortPref; + + // 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"); + ftpPref = dialog.Preferences.get("network.proxy.ftp"); + ftpPortPref = dialog.Preferences.get("network.proxy.ftp_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; + ftpPref.value = "localhost"; + ftpPortPref.value = 80; + dialogElement.acceptDialog(); + + // Testing HTTP port 80 + FTP port 0 with share off + httpPortPref.value = 80; + ftpPortPref.value = 0; + 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; + ftpPortPref.value = 80; + dialogElement.acceptDialog(); + + // HTTP 80, FTP 0, with share on + await setDoc(); + proxyTypePref.value = 1; + sharePref.value = true; + ftpPref.value = "localhost"; + httpPref.value = "localhost"; + httpPortPref.value = 80; + ftpPortPref.value = 0; + 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_connection_dnsoverhttps.js b/browser/components/preferences/tests/browser_connection_dnsoverhttps.js new file mode 100644 index 0000000000..e7adabe6c0 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_dnsoverhttps.js @@ -0,0 +1,496 @@ +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "DoHController", + "resource:///modules/DoHController.jsm" +); + +const SUBDIALOG_URL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; +const TRR_MODE_PREF = "network.trr.mode"; +const TRR_URI_PREF = "network.trr.uri"; +const TRR_RESOLVERS_PREF = "network.trr.resolvers"; +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 DEFAULT_RESOLVER_VALUE = "https://mozilla.cloudflare-dns.com/dns-query"; +const NEXTDNS_RESOLVER_VALUE = "https://firefox.dns.nextdns.io/"; + +const modeCheckboxSelector = "#networkDnsOverHttps"; +const uriTextboxSelector = "#networkCustomDnsOverHttpsInput"; +const resolverMenulistSelector = "#networkDnsOverHttpsResolverChoices"; +const defaultPrefValues = Object.freeze({ + [TRR_MODE_PREF]: 0, + [TRR_URI_PREF]: "https://mozilla.cloudflare-dns.com/dns-query", + [TRR_RESOLVERS_PREF]: JSON.stringify([ + { name: "Cloudflare", url: DEFAULT_RESOLVER_VALUE }, + { name: "example.org", url: "https://example.org/dns-query" }, + ]), + [TRR_CUSTOM_URI_PREF]: "", +}); + +async function resetPrefs() { + await DoHController._uninit(); + Services.prefs.clearUserPref(TRR_MODE_PREF); + Services.prefs.clearUserPref(TRR_URI_PREF); + Services.prefs.clearUserPref(TRR_RESOLVERS_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(); +} + +let preferencesOpen = new Promise(res => open_preferences(res)); + +registerCleanupFunction(() => { + resetPrefs(); + gBrowser.removeCurrentTab(); +}); + +async function openConnectionsSubDialog() { + /* + The connection dialog has type="child", So it has to be opened as a sub dialog + of the main pref tab. Prefs only get updated after the subdialog is confirmed & closed + */ + let dialog = await openAndLoadSubDialog(SUBDIALOG_URL); + ok(dialog, "connection window opened"); + return dialog; +} + +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]); + } + if (props.hasOwnProperty(TRR_RESOLVERS_PREF)) { + info(`Setting ${TRR_RESOLVERS_PREF} to ${props[TRR_RESOLVERS_PREF]}`); + Services.prefs.setStringPref(TRR_RESOLVERS_PREF, props[TRR_RESOLVERS_PREF]); + } + + let dialog = await openConnectionsSubDialog(); + await dialog.uiReady; + info( + Date.now() - startTime + ": testWithProperties: connections dialog now open" + ); + let doc = dialog.document; + let win = doc.ownerGlobal; + let dialogElement = doc.getElementById("ConnectionsDialog"); + let dialogClosingPromise = BrowserTestUtils.waitForEvent( + dialogElement, + "dialogclosing" + ); + let modeCheckbox = doc.querySelector(modeCheckboxSelector); + let uriTextbox = doc.querySelector(uriTextboxSelector); + let resolverMenulist = doc.querySelector(resolverMenulistSelector); + let uriPrefChangedPromise; + let modePrefChangedPromise; + let disableHeuristicsPrefChangedPromise; + + if (props.hasOwnProperty("expectedModeChecked")) { + await TestUtils.waitForCondition( + () => modeCheckbox.checked === props.expectedModeChecked + ); + is( + modeCheckbox.checked, + props.expectedModeChecked, + "mode checkbox has expected checked state" + ); + } + 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) { + 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" + ); + modeCheckbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter(modeCheckbox, {}, win); + info( + Date.now() - + startTime + + ": testWithProperties: clickMode, mouse click synthesized" + ); + } + if (props.hasOwnProperty("selectResolver")) { + 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("change", { bubbles: true })); + info( + Date.now() - + startTime + + ": testWithProperties: selectResolver, item value set and events dispatched" + ); + } + + 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" + ); + 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: calling acceptDialog"); + dialogElement.acceptDialog(); + + info( + Date.now() - + startTime + + ": testWithProperties: waiting for the dialogClosingPromise" + ); + let dialogClosingEvent = await dialogClosingPromise; + ok(dialogClosingEvent, "connection window closed"); + + 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")) { + let uriPref = Services.prefs.getStringPref(TRR_URI_PREF); + is( + uriPref, + props.expectedFinalUriPref, + "uri pref ended up with the expected value" + ); + } + + 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("expectedFinalCusomUriPref")) { + let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF); + is( + customUriPref, + props.expectedFinalCustomUriPref, + "custom_uri pref ended up with the expected value" + ); + } + + info(Date.now() - startTime + ": testWithProperties: fin"); +} + +add_task(async function default_values() { + let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF); + let uriPref = Services.prefs.getStringPref(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` + ); + is( + uriPref, + defaultPrefValues[TRR_URI_PREF], + `Actual value of ${TRR_URI_PREF} matches expected default value` + ); + is( + customUriPref, + defaultPrefValues[TRR_CUSTOM_URI_PREF], + `Actual value of ${TRR_CUSTOM_URI_PREF} matches expected default value` + ); +}); + +let testVariations = [ + // verify state with defaults + { name: "default", expectedModePref: 0, expectedUriValue: "" }, + + // verify each of the modes maps to the correct checked state + { name: "mode 0", [TRR_MODE_PREF]: 0, expectedModeChecked: false }, + { + name: "mode 1", + [TRR_MODE_PREF]: 1, + expectedModeChecked: false, + expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, + }, + { + name: "mode 2", + [TRR_MODE_PREF]: 2, + expectedModeChecked: true, + expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, + }, + { + name: "mode 3", + [TRR_MODE_PREF]: 3, + expectedModeChecked: true, + expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, + }, + { + name: "mode 4", + [TRR_MODE_PREF]: 4, + expectedModeChecked: false, + expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, + }, + { name: "mode 5", [TRR_MODE_PREF]: 5, expectedModeChecked: false }, + // verify an out of bounds mode value maps to the correct checked state + { + name: "mode out-of-bounds", + [TRR_MODE_PREF]: 77, + expectedModeChecked: false, + }, + + // verify automatic heuristics states + { + name: "heuristics on and mode unset", + [TRR_MODE_PREF]: 0, + [ROLLOUT_ENABLED_PREF]: true, + expectedModeChecked: true, + }, + { + name: "heuristics on and mode set to 2", + [TRR_MODE_PREF]: 2, + [ROLLOUT_ENABLED_PREF]: true, + expectedModeChecked: true, + }, + { + name: "heuristics on but disabled, mode unset", + [TRR_MODE_PREF]: 5, + [ROLLOUT_ENABLED_PREF]: true, + expectedModeChecked: false, + }, + { + name: "heuristics on but disabled, mode set to 2", + [TRR_MODE_PREF]: 2, + [ROLLOUT_ENABLED_PREF]: true, + expectedModeChecked: true, + }, + + // verify toggling the checkbox gives the right outcomes + { + name: "toggle mode on", + clickMode: true, + expectedModeValue: 2, + expectedUriValue: "", + expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, + }, + { + name: "toggle mode off", + [TRR_MODE_PREF]: 2, + expectedModeChecked: true, + clickMode: true, + expectedModePref: 5, + }, + { + name: "toggle mode off when on due to heuristics", + [TRR_MODE_PREF]: 0, + [ROLLOUT_ENABLED_PREF]: true, + expectedModeChecked: true, + clickMode: true, + expectedModePref: 5, + expectedDisabledHeuristics: true, + }, + // Test selecting non-default, non-custom TRR provider, NextDNS. + { + name: "Select NextDNS as TRR provider", + [TRR_MODE_PREF]: 2, + selectResolver: NEXTDNS_RESOLVER_VALUE, + expectedFinalUriPref: NEXTDNS_RESOLVER_VALUE, + }, + { + name: "return to default from NextDNS", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: NEXTDNS_RESOLVER_VALUE, + expectedResolverListValue: NEXTDNS_RESOLVER_VALUE, + selectResolver: DEFAULT_RESOLVER_VALUE, + expectedFinalUriPref: DEFAULT_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: true, + 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: true, + selectResolver: "custom", + inputUriKeys: "https://example.com", + expectedModePref: 2, + expectedFinalUriPref: "https://example.com", + expectedFinalCustomUriPref: "https://example.com", + }, + + { + name: "return to default from custom", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "https://example.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: DEFAULT_RESOLVER_VALUE, + expectedFinalCustomUriPref: "", + }, + { + name: "empty default resolver list", + [TRR_RESOLVERS_PREF]: "", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "", + [TRR_RESOLVERS_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() { + await preferencesOpen; + let startTime = Date.now(); + await resetPrefs(); + info("starting test: " + props.name); + await testWithProperties(props, startTime); + }); +} 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..a59ea35b19 --- /dev/null +++ b/browser/components/preferences/tests/browser_containers_name_input.js @@ -0,0 +1,65 @@ +const CONTAINERS_URL = + "chrome://browser/content/preferences/dialogs/containers.xhtml"; + +add_task(async function setup() { + 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" + ); +}); diff --git a/browser/components/preferences/tests/browser_contentblocking.js b/browser/components/preferences/tests/browser_contentblocking.js new file mode 100644 index 0000000000..0a7738da03 --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking.js @@ -0,0 +1,973 @@ +/* eslint-env webextensions */ + +ChromeUtils.defineModuleGetter( + this, + "Preferences", + "resource://gre/modules/Preferences.jsm" +); + +const TP_PREF = "privacy.trackingprotection.enabled"; +const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled"; +const NCB_PREF = "network.cookie.cookieBehavior"; +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 LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled"; +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.import( + "resource://testing-common/EnterprisePolicyTesting.jsm", + null +); + +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], + [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 private = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=private]" + ); + menu.selectedItem = always; + ok( + !private.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( + private.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}` + ); + // 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}` + ); + // 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}` + ); + // 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}` + ); + // 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}` + ); + + 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, + [FP_PREF]: null, + [STP_PREF]: null, + [CM_PREF]: null, + [LEVEL2_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.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( + LEVEL2_PREF, + !Services.prefs.getBoolPref(LEVEL2_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(LEVEL2_PREF, false); + Services.prefs.setIntPref( + NCB_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 "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 "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; + 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 prefs = [TP_PREF, TP_PBM_PREF, NCB_PREF, FP_PREF, STP_PREF, CM_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 does not force changes of any prefs, other than CAT_PREF, all other TP prefs should remain as they were for standard. + for (let pref of prefs) { + 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 FP_PREF, STP_PREF, CM_PREF, TP_PREF, or TP_PBM_PREF should necessarily set CAT_PREF to "custom" + for (let pref of [FP_PREF, STP_PREF, CM_PREF, TP_PREF, TP_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` + ); + + for (let pref of prefs) { + 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 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` + ); + + 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 + ); + 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.loadURI(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.loadURI(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..148de06514 --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking_categories.js @@ -0,0 +1,268 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env webextensions */ + +ChromeUtils.defineModuleGetter( + this, + "Preferences", + "resource://gre/modules/Preferences.jsm" +); + +const TP_PREF = "privacy.trackingprotection.enabled"; +const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled"; +const NCB_PREF = "network.cookie.cookieBehavior"; +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 LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled"; +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(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_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 originalNCB = defaults.getIntPref(NCB_PREF); + let originalLEVEL2 = defaults.getBoolPref(LEVEL2_PREF); + + let nonDefaultNCB; + switch (originalNCB) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + default: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + } + defaults.setIntPref(NCB_PREF, nonDefaultNCB); + 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.setIntPref(NCB_PREF, !originalNCB); + defaults.setBoolPref(LEVEL2_PREF, !originalLEVEL2); + + 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(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_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.setIntPref(NCB_PREF, originalNCB); + defaults.setBoolPref(LEVEL2_PREF, originalLEVEL2); +}); + +// 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,stp,lvl2" + ); + 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,stp,lvl2", + `${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.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCB_PREF} has been set to BEHAVIOR_REJECT_TRACKER` + ); + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + true, + `${LEVEL2_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(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_PREF} pref has the default value` + ); + + defaults.setStringPref( + STRICT_DEF_PREF, + "-tpPrivate,-fp,-cm,-tp,cookieBehavior3,-stp,-lvl2" + ); + 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.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCB_PREF} has been set to BEHAVIOR_REJECT_TRACKER` + ); + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + false, + `${LEVEL2_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_cookie_exceptions_addRemove.js b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js new file mode 100644 index 0000000000..05755792df --- /dev/null +++ b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js @@ -0,0 +1,296 @@ +/* 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"; + +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..19d78c5d1c --- /dev/null +++ b/browser/components/preferences/tests/browser_cookies_exceptions.js @@ -0,0 +1,587 @@ +/* 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, + }, + ]; + } + ); +}); + +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); +} + +function createObserveAllPromise(observances) { + return new Promise(resolve => { + let permObserver = { + observe(aSubject, aTopic, aData) { + if (aTopic != "perm-changed") { + return; + } + + if (!observances.length) { + // Should fail here as we are not expecting a notification, but we + // don't. See bug 1063410. + return; + } + + info(`observed perm-changed (remaining ${observances.length - 1})`); + + let permission = aSubject.QueryInterface(Ci.nsIPermission); + let expected = observances.shift(); + + is(aData, expected.data, "type of message should be the same"); + for (let prop of ["type", "capability"]) { + if (expected[prop]) { + is( + permission[prop], + expected[prop], + 'property: "' + prop + '" should be equal' + ); + } + } + + if (expected.origin) { + is( + permission.principal.origin, + expected.origin, + 'property: "origin" should be equal' + ); + } + + if (!observances.length) { + Services.obs.removeObserver(permObserver, "perm-changed"); + executeSoon(resolve); + } + }, + }; + Services.obs.addObserver(permObserver, "perm-changed"); + }); +} 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..f157e30bd3 --- /dev/null +++ b/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js @@ -0,0 +1,170 @@ +"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 setDefaultPane = content.document.getElementById("setDefaultPane"); + Assert.equal( + setDefaultPane.selectedIndex, + "0", + "The 'make default' pane should be visible when not default" + ); + 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" + ); + Assert.equal( + setDefaultPane.selectedIndex, + "1", + "The 'make default' pane should not be visible when default" + ); + 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 setDefaultPane = content.document.getElementById("setDefaultPane"); + Assert.equal( + setDefaultPane.selectedIndex, + "0", + "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( + () => setDefaultPane.selectedIndex == "1", + "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..3255d36956 --- /dev/null +++ b/browser/components/preferences/tests/browser_engines.js @@ -0,0 +1,95 @@ +let engineName = "engine1"; + +function getCellText(tree, i, cellName) { + return tree.view.getCellText(i, tree.columns.getNamedColumn(cellName)); +} + +add_task(async function setup() { + const engine = await Services.search.addEngineWithDetails(engineName, { + alias: ["testing", "customkeyword"], + method: "get", + template: "http://example.com/engine1?search={searchTerms}", + }); + registerCleanupFunction(async function() { + await Services.search.removeEngine(engine); + }); +}); + +// Test Engine list +add_task(async function() { + 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); +}); 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..9c753b6e50 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features_filter.js @@ -0,0 +1,176 @@ +/* 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) { + doc.getElementById(definition.id).label = definition.title; + doc.getElementById(definition.id + "-description").textContent = + definition.description; + } + + // First, check that all of the items are visible by default. + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + "item " + definition.id + " not initially hidden" + ); + } + + // After searching, only a subset should be visible. + await enterSearch(doc, "feature"); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + definition.result, + "item " + definition.id + " after first search" + ); + } + + // 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, + "item " + definition.id + " 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, + "item " + definition.id + " not hidden 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, + "item " + definition.id + " after next search" + ); + } + + EventUtils.synthesizeMouseAtCenter( + doc.getElementById(category), + {}, + gBrowser.contentWindow + ); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + "item " + + definition.id + + " not hidden 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..fbcba93a4e --- /dev/null +++ b/browser/components/preferences/tests/browser_extension_controlled.js @@ -0,0 +1,1408 @@ +/* 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.defineModuleGetter( + this, + "ExtensionSettingsStore", + "resource://gre/modules/ExtensionSettingsStore.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "AboutNewTab", + "resource:///modules/AboutNewTab.jsm" +); + +XPCOMUtils.defineLazyPreferenceGetter(this, "proxyType", PROXY_PREF); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +AddonTestUtils.initMochitest(this); + +const { ExtensionPreferencesManager } = ChromeUtils.import( + "resource://gre/modules/ExtensionPreferencesManager.jsm" +); + +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", + applications: { + 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", + applications: { + 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", + applications: { + 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", + applications: { + 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", + applications: { + 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", + applications: { 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-controlled-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(await PathUtils.getProfileDir(), 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-controlled-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", + applications: { 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", + applications: { 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-controlled-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-controlled-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-controlled-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 = 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", + applications: { 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); +}); 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..458d7ee2c3 --- /dev/null +++ b/browser/components/preferences/tests/browser_filetype_dialog.js @@ -0,0 +1,192 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +SimpleTest.requestCompleteLog(); +ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm", + this +); + +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_task(async function setup() { + // 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..4e3216549b --- /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_healthreport.js b/browser/components/preferences/tests/browser_healthreport.js new file mode 100644 index 0000000000..64be23dadb --- /dev/null +++ b/browser/components/preferences/tests/browser_healthreport.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +function runPaneTest(fn) { + open_preferences(async win => { + let doc = win.document; + await win.gotoPref("paneAdvanced"); + let advancedPrefs = doc.getElementById("advancedPrefs"); + let tab = doc.getElementById("dataChoicesTab"); + advancedPrefs.selectedTab = tab; + fn(win, doc); + }); +} + +function test() { + waitForExplicitFinish(); + resetPreferences(); + registerCleanupFunction(resetPreferences); + runPaneTest(testBasic); +} + +function testBasic(win, doc) { + is( + Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), + true, + "Health Report upload enabled on app first run." + ); + + let checkbox = doc.getElementById("submitHealthReportBox"); + ok(checkbox); + is( + checkbox.checked, + true, + "Health Report checkbox is checked on app first run." + ); + + checkbox.checked = false; + checkbox.doCommand(); + is( + Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), + false, + "Unchecking checkbox opts out of FHR upload." + ); + + checkbox.checked = true; + checkbox.doCommand(); + is( + Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), + true, + "Checking checkbox allows FHR upload." + ); + + win.close(); + Services.prefs.lockPref(FHR_UPLOAD_ENABLED); + runPaneTest(testUploadDisabled); +} + +function testUploadDisabled(win, doc) { + ok( + Services.prefs.prefIsLocked(FHR_UPLOAD_ENABLED), + "Upload enabled flag is locked." + ); + let checkbox = doc.getElementById("submitHealthReportBox"); + is( + checkbox.getAttribute("disabled"), + "true", + "Checkbox is disabled if upload flag is locked." + ); + Services.prefs.unlockPref(FHR_UPLOAD_ENABLED); + + win.close(); + finish(); +} + +function resetPreferences() { + Services.prefs.clearUserPref(FHR_UPLOAD_ENABLED); +} 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..62788ca03b --- /dev/null +++ b/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js @@ -0,0 +1,35 @@ +var { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm"); + +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..7f839c6de6 --- /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_task(async function setup() { + 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..8986257678 --- /dev/null +++ b/browser/components/preferences/tests/browser_hometab_restore_defaults.js @@ -0,0 +1,212 @@ +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 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 })), + ], + // 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 })), + ], + ], + }); + + 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 === "visible" + ), + "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_section.js b/browser/components/preferences/tests/browser_https_only_section.js new file mode 100644 index 0000000000..5c20d87a66 --- /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_languages_subdialog.js b/browser/components/preferences/tests/browser_languages_subdialog.js new file mode 100644 index 0000000000..d14df76445 --- /dev/null +++ b/browser/components/preferences/tests/browser_languages_subdialog.js @@ -0,0 +1,112 @@ +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; + win.Preferences.forceEnableInstantApply(); + dialogOverlay = content.gSubDialog._topDialog._overlay; + ok(!BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is visible."); + return win; + } + + function closeLanguagesSubdialog() { + const closeBtn = dialogOverlay.querySelector(".dialogClose"); + closeBtn.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." + ); + closeLanguagesSubdialog(); + ok(BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is invisible."); + + 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." + ); + closeLanguagesSubdialog(); + + 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." + ); + closeLanguagesSubdialog(); + + 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." + ); + closeLanguagesSubdialog(); + + 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..356bdd1831 --- /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..613ac7c935 --- /dev/null +++ b/browser/components/preferences/tests/browser_localSearchShortcuts.js @@ -0,0 +1,303 @@ +/* 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"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", + UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", +}); + +let gTree; + +add_task(async function init() { + 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; + 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_masterpassword.js b/browser/components/preferences/tests/browser_masterpassword.js new file mode 100644 index 0000000000..39f690cb71 --- /dev/null +++ b/browser/components/preferences/tests/browser_masterpassword.js @@ -0,0 +1,129 @@ +ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", this); +ChromeUtils.import("resource://gre/modules/OSKeyStore.jsm", this); +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +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; + masterPasswordSet = masterPasswordNextState; + aCallback(); + }, + })); + + let masterPasswordSet = false; + win.LoginHelper = { + isMasterPasswordSet() { + return masterPasswordSet; + }, + }; + + let checkbox = doc.querySelector("#useMasterPassword"); + checkbox.scrollIntoView(); + ok( + !checkbox.checked, + "master password checkbox should be unchecked by default" + ); + let button = doc.getElementById("changeMasterPassword"); + ok(button.disabled, "master password button should be disabled by default"); + + let masterPasswordNextState = 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, + "master password checkbox should be unchecked after canceling os auth dialog" + ); + ok(button.disabled, "button should be disabled after canceling os auth"); + } + + masterPasswordNextState = 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 masterpassword dialog" + ); + } else { + masterPasswordSet = true; + doc.defaultView.gPrivacyPane._initMasterPasswordUI(); + await TestUtils.waitForCondition( + () => !button.disabled, + "waiting for master password button to get enabled" + ); + } + ok(!button.disabled, "master password button should now be enabled"); + ok(checkbox.checked, "master 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 masterpassword dialog" + ); + ok(!button.disabled, "master password button should still be enabled"); + ok(checkbox.checked, "master 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" + ); + + masterPasswordNextState = false; + dialogURL = ""; + checkbox.click(); + is( + dialogURL, + "chrome://mozapps/content/preferences/removemp.xhtml", + "clicking on the checkbox to uncheck master password should show the removal dialog" + ); + ok(button.disabled, "master password button should now be disabled"); + ok(!checkbox.checked, "master password checkbox should now be unchecked"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_media_control.js b/browser/components/preferences/tests/browser_media_control.js new file mode 100644 index 0000000000..6512fb9e74 --- /dev/null +++ b/browser/components/preferences/tests/browser_media_control.js @@ -0,0 +1,83 @@ +/** + * Media control check box should change the media control pref, vice versa. + */ +const MEDIA_CONTROL_PREF = "media.hardwaremediakeys.enabled"; + +add_task(async function testMediaControlCheckBox() { + const prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + const checkBox = gBrowser.contentDocument.getElementById( + "mediaControlToggleEnabled" + ); + ok(checkBox, "check box exists"); + + // The pref is true by default. + await modifyPrefAndWaitUntilCheckBoxChanges(false); + await modifyPrefAndWaitUntilCheckBoxChanges(true); + await toggleCheckBoxAndWaitUntilPrefValueChanges(false); + await toggleCheckBoxAndWaitUntilPrefValueChanges(true); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +async function modifyPrefAndWaitUntilCheckBoxChanges(isEnabled) { + info((isEnabled ? "enable" : "disable") + " the pref"); + const checkBox = gBrowser.contentDocument.getElementById( + "mediaControlToggleEnabled" + ); + await SpecialPowers.pushPrefEnv({ + set: [[MEDIA_CONTROL_PREF, isEnabled]], + }); + await TestUtils.waitForCondition( + _ => checkBox.checked == isEnabled, + "Waiting for the checkbox gets checked" + ); + is(checkBox.checked, isEnabled, `check box status is correct`); + checkAndClearTelemetryProbe(isEnabled); +} + +async function toggleCheckBoxAndWaitUntilPrefValueChanges(isChecked) { + info((isChecked ? "check" : "uncheck") + " the check box"); + const checkBox = gBrowser.contentDocument.getElementById( + "mediaControlToggleEnabled" + ); + checkBox.click(); + is( + Services.prefs.getBoolPref(MEDIA_CONTROL_PREF), + isChecked, + "the pref's value is correct" + ); + checkAndClearTelemetryProbe(isChecked, true /* check UI */); +} + +/** + * These telemetry related variable and method should be removed after the + * telemetry probe `MEDIA_CONTROL_SETTING_CHANGE` gets expired. + */ +const HISTOGRAM_ID = "MEDIA_CONTROL_SETTING_CHANGE"; +const HISTOGRAM_KEYS = { + EnableFromUI: 0, + EnableTotal: 1, + DisableFromUI: 2, + DisableTotal: 3, +}; + +function checkAndClearTelemetryProbe(isEnable, checkUI = false) { + const histogram = Services.telemetry.getHistogramById(HISTOGRAM_ID); + let keyTotal = isEnable ? "EnableTotal" : "DisableTotal"; + let keyUI = null; + if (checkUI) { + keyUI = isEnable ? "EnableFromUI" : "DisableFromUI"; + } + for (let [key, val] of Object.entries(histogram.snapshot().values)) { + if (key == HISTOGRAM_KEYS[keyTotal]) { + ok(val, "Increase the amount for the probe 'changeing total setting'"); + } + if (keyUI && key == HISTOGRAM_KEYS[keyUI]) { + ok(val, "Increase the amount for the probe 'changeing setting from UI'"); + } + } + histogram.clear(); +} 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..53a57dfbec --- /dev/null +++ b/browser/components/preferences/tests/browser_newtab_menu.js @@ -0,0 +1,37 @@ +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, + 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..68e40c5e92 --- /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_password_management.js b/browser/components/preferences/tests/browser_password_management.js new file mode 100644 index 0000000000..96d7bbf2a6 --- /dev/null +++ b/browser/components/preferences/tests/browser_password_management.js @@ -0,0 +1,39 @@ +"use strict"; + +ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this); +ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm", this); + +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_performance.js b/browser/components/preferences/tests/browser_performance.js new file mode 100644 index 0000000000..00329269cc --- /dev/null +++ b/browser/components/preferences/tests/browser_performance.js @@ -0,0 +1,318 @@ +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" + ); + + let contentProcessCountEnabledDescription = doc.querySelector( + "#contentProcessCountEnabledDescription" + ); + is( + contentProcessCountEnabledDescription.hidden, + false, + "process count enabled description should be shown" + ); + + let contentProcessCountDisabledDescription = doc.querySelector( + "#contentProcessCountDisabledDescription" + ); + is( + contentProcessCountDisabledDescription.hidden, + true, + "process count enabled description should be hidden" + ); + + 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_e10srollout.js b/browser/components/preferences/tests/browser_performance_e10srollout.js new file mode 100644 index 0000000000..4b6025ee47 --- /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..067fbfdc64 --- /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..de93e61448 --- /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", "btnSession", "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..b89b3d7707 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_dialog.js @@ -0,0 +1,627 @@ +"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/. */ + +var { SitePermissions } = ChromeUtils.import( + "resource:///modules/SitePermissions.jsm" +); + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml"; +const URL = "http://www.example.com"; +const URI = Services.io.newURI(URL); +var sitePermissionsDialog; + +function checkPermissionItem(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() { + 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; +} + +add_task(async function openSitePermissionsDialog() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openPermissionsDialog(); +}); + +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); + checkPermissionItem(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 + ); + + checkPermissionItem(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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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 removeTab() { + gBrowser.removeCurrentTab(); +}); 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_privacy_passwordGenerationAndAutofill.js b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js new file mode 100644 index 0000000000..94b7015b88 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js @@ -0,0 +1,200 @@ +const { TestUtils } = ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); + +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_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..16756dd67d --- /dev/null +++ b/browser/components/preferences/tests/browser_proxy_backup.js @@ -0,0 +1,83 @@ +/* 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/. */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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("browser.preferences.instantApply"); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + for (let proxyType of ["http", "ssl", "ftp", "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" + ); + } + }); + + 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..d927423c24 --- /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_task(function setup() { + 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_searchDefaultEngine.js b/browser/components/preferences/tests/browser_searchDefaultEngine.js new file mode 100644 index 0000000000..ab02bee3b7 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchDefaultEngine.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SearchTestUtils } = ChromeUtils.import( + "resource://testing-common/SearchTestUtils.jsm" +); + +add_task(async function setup() { + const engine1 = await Services.search.addEngineWithDetails("engine1", { + method: "get", + template: "http://example.com/engine1?search={searchTerms}", + }); + + const engine2 = await Services.search.addEngineWithDetails("engine2", { + method: "get", + template: "http://example.com/engine2?search={searchTerms}", + }); + + const originalDefault = await Services.search.getDefault(); + const originalDefaultPrivate = await Services.search.getDefaultPrivate(); + + registerCleanupFunction(async () => { + await Services.search.setDefault(originalDefault); + await Services.search.setDefaultPrivate(originalDefaultPrivate); + + await Services.search.removeEngine(engine1); + await Services.search.removeEngine(engine2); + }); +}); + +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); + + await setDefaultEngine(false, "engine1", "engine2"); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_setPrivateDefaultEngine() { + 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); + + await setDefaultEngine(true, "engine2", "engine1"); + + 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..3d69addab1 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchRestoreDefaults.js @@ -0,0 +1,192 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SearchTestUtils } = ChromeUtils.import( + "resource://testing-common/SearchTestUtils.jsm" +); +const { SearchUtils } = ChromeUtils.import( + "resource://gre/modules/SearchUtils.jsm" +); +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 final + // remaining engine, which by design can't be removed. + for (let i = 0; i < defaultEngines.length - 3; i++) { + tree.view.selection.select(0); + 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(); +}); diff --git a/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js new file mode 100644 index 0000000000..0826000abe --- /dev/null +++ b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_NAME = "browser.urlbar.matchBuckets"; +const HISTORY_FIRST_PREF_VALUE = "general:5,suggestion:Infinity"; +const CHECKBOX_ID = "showSearchSuggestionsFirstCheckbox"; + +// Open preferences with search suggestions shown first (the default). +add_task(async function openWithSearchSuggestionsShownFirst() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + + // The pref should be cleared initially so that search suggestions are shown + // first (the default). + Assert.equal( + Services.prefs.getCharPref(PREF_NAME, ""), + "", + "Pref should be cleared initially" + ); + + // Open preferences. The checkbox should be checked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(CHECKBOX_ID); + Assert.equal(checkbox.checked, true, "Checkbox should be checked"); + + // Uncheck the checkbox. + checkbox.checked = false; + checkbox.doCommand(); + + // The pref should now be set so that history is shown first. + Assert.equal( + Services.prefs.getCharPref(PREF_NAME, ""), + HISTORY_FIRST_PREF_VALUE, + "Pref should now be set to show history first" + ); + + // Clear the pref. + Services.prefs.clearUserPref(PREF_NAME); + + // The checkbox should have become checked again. + Assert.equal( + checkbox.checked, + true, + "Checkbox should become checked 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.setCharPref(PREF_NAME, HISTORY_FIRST_PREF_VALUE); + + // Open preferences. The checkbox should be unchecked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(CHECKBOX_ID); + Assert.equal(checkbox.checked, false, "Checkbox should be unchecked"); + + // Check the checkbox. + checkbox.checked = true; + checkbox.doCommand(); + + // The pref should now be cleared so that search suggestions are shown first. + Assert.equal( + Services.prefs.getCharPref(PREF_NAME, ""), + "", + "Pref should now be cleared to show search suggestions first" + ); + + // Set the pref again. + Services.prefs.setCharPref(PREF_NAME, HISTORY_FIRST_PREF_VALUE); + + // The checkbox should have become unchecked again. + Assert.equal( + checkbox.checked, + false, + "Checkbox should become unchecked after setting pref" + ); + + // Clean up. + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref(PREF_NAME); +}); 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..d629f26f13 --- /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_subdialogs_within_preferences_1.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js new file mode 100644 index 0000000000..f11583960c --- /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..07b510801d --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js @@ -0,0 +1,33 @@ +/* + * 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’ve turned off protections on these websites." #permissions-exceptions-etp-desc + */ +add_task(async function() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("turned off protections", "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..cb998a4d0a --- /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..12b0278178 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js @@ -0,0 +1,32 @@ +/* + * 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() { + 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..f0f5cb1424 --- /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", "fontsGroup"); + 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..3ddb018bfb --- /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..b644346112 --- /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..3b6a301172 --- /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..ef9ff9abe4 --- /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("third-party", ["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..5655d8db39 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_1.js @@ -0,0 +1,333 @@ +"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" + ) { + 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; + + // Checks if back to generalPane + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if ( + child.id == "paneGeneral" || + child.id == "startupGroup" || + child.id == "languagesGroup" || + child.id == "fontsGroup" || + child.id == "zoomGroup" || + child.id == "downloadsGroup" || + child.id == "applicationsGroup" || + child.id == "drmGroup" || + child.id == "updateApp" || + child.id == "browsingGroup" || + child.id == "performanceGroup" || + child.id == "connectionGroup" || + child.id == "generalCategory" || + child.id == "languageAndAppearanceCategory" || + child.id == "filesAndApplicationsCategory" || + child.id == "updatesCategory" || + child.id == "performanceCategory" || + child.id == "browsingCategory" || + child.id == "networkProxyCategory" + ) { + is_element_visible(child, "Should be in general tab"); + } 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..25afe53c26 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_2.js @@ -0,0 +1,171 @@ +"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; + + 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..df9e611256 --- /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..d304b630c2 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchsuggestions.js @@ -0,0 +1,129 @@ +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_task(async function setup() { + 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..31cd91b69d --- /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_task(async function setup() { + 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..f7748df682 --- /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_task(async function setup() { + 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_site_login_exceptions.js b/browser/components/preferences/tests/browser_site_login_exceptions.js new file mode 100644 index 0000000000..3404d68149 --- /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_spotlight.js b/browser/components/preferences/tests/browser_spotlight.js new file mode 100644 index 0000000000..c48b7270a6 --- /dev/null +++ b/browser/components/preferences/tests/browser_spotlight.js @@ -0,0 +1,64 @@ +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.getBoolPref( + "extensions.formautofill.creditCards.available" + ) + ) { + 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_strings.js b/browser/components/preferences/tests/browser_statePartitioning_strings.js new file mode 100644 index 0000000000..62bcb027b1 --- /dev/null +++ b/browser/components/preferences/tests/browser_statePartitioning_strings.js @@ -0,0 +1,113 @@ +"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 MVP_UI_PREF = "browser.contentblocking.state-partitioning.mvp.ui.enabled"; +const FPI_PREF = "privacy.firstparty.isolate"; +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIOR_VALUE = 5; + +async function testStrings(mvpUIEnabled) { + info(`Running testStrings with MVP UI pref set to ${MVP_UI_PREF}`); + + SpecialPowers.pushPrefEnv({ set: [[MVP_UI_PREF, mvpUIEnabled]] }); + 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) { + is( + elt.hidden, + !mvpUIEnabled, + `The new cross-site cookies info label is ${ + mvpUIEnabled ? "visible" : "hidden" + }` + ); + } + + elts = doc.querySelectorAll( + ".extra-information-label.third-party-tracking-cookies-plus-isolate-option" + ); + for (let elt of elts) { + is( + elt.hidden, + mvpUIEnabled, + `The old third party cookies info label is ${ + mvpUIEnabled ? "hidden" : "visible" + }` + ); + } + + // Check the learn more strings + elts = doc.querySelectorAll( + ".tail-with-learn-more.content-blocking-warning-description" + ); + let expectedStringID = mvpUIEnabled + ? "content-blocking-and-isolating-etp-warning-description-2" + : "content-blocking-and-isolating-etp-warning-description"; + for (let elt of elts) { + let id = doc.l10n.getAttributes(elt).id; + is( + id, + expectedStringID, + "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; + expectedStringID = mvpUIEnabled + ? "sitedata-option-block-cross-site-cookies-including-social-media" + : "sitedata-option-block-cross-site-and-social-media-trackers-plus-isolate"; + is( + id, + expectedStringID, + "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"); + is( + warningElt.hidden, + !mvpUIEnabled, + `The FPI warning is ${mvpUIEnabled ? "visible" : "hidden"}` + ); + 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(true); + await testStrings(false); +}); diff --git a/browser/components/preferences/tests/browser_subdialogs.js b/browser/components/preferences/tests/browser_subdialogs.js new file mode 100644 index 0000000000..559b58ee9f --- /dev/null +++ b/browser/components/preferences/tests/browser_subdialogs.js @@ -0,0 +1,594 @@ +/* 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() { + let frameStyle = content.window.gSubDialog._topDialog._frame.style; + Assert.equal( + frameStyle.width, + "32em", + "Width should be set on the frame from the dialog" + ); + Assert.equal( + frameStyle.height, + "5em", + "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 + ) { + let frame = content.window.gSubDialog._topDialog._frame; + let docEl = frame.contentDocument.documentElement; + Assert.equal( + frame.style.width, + "32em", + "Width should be set on the frame from the dialog" + ); + Assert.ok( + docEl.scrollHeight > contentOldHeight, + "Content height increased (from " + + contentOldHeight + + " to " + + docEl.scrollHeight + + ")." + ); + Assert.equal( + frame.style.height, + docEl.scrollHeight + "px", + "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() { + let frame = content.window.gSubDialog._topDialog._frame; + Assert.equal( + frame.style.width, + "32em", + "Width should be set on the frame from the dialog" + ); + Assert.ok( + parseInt(frame.style.height, 10) < 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" + ); + Assert.ok( + frame.style.height.endsWith("px"), + "Height (" + + frame.style.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_disabled.js b/browser/components/preferences/tests/browser_sync_disabled.js new file mode 100644 index 0000000000..9ab91564d4 --- /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..635e21abad --- /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.import( + "resource://services-sync/UIState.jsm", + {} +); +const { FxAccountsPairingFlow } = ChromeUtils.import( + "resource://gre/modules/FxAccountsPairing.jsm", + {} +); + +// Use sinon for mocking. +const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +let flowCounter = 0; + +add_task(async function setup() { + 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_telemetry.js b/browser/components/preferences/tests/browser_telemetry.js new file mode 100644 index 0000000000..fbd44db3e7 --- /dev/null +++ b/browser/components/preferences/tests/browser_telemetry.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; + +function runPaneTest(fn) { + open_preferences(async win => { + let doc = win.document; + await win.gotoPref("paneAdvanced"); + let advancedPrefs = doc.getElementById("advancedPrefs"); + let tab = doc.getElementById("dataChoicesTab"); + advancedPrefs.selectedTab = tab; + fn(win, doc); + }); +} + +function test() { + waitForExplicitFinish(); + resetPreferences(); + registerCleanupFunction(resetPreferences); + runPaneTest(testTelemetryState); +} + +function testTelemetryState(win, doc) { + let fhrCheckbox = doc.getElementById("submitHealthReportBox"); + Assert.ok( + fhrCheckbox.checked, + "Health Report checkbox is checked on app first run." + ); + + let telmetryCheckbox = doc.getElementById("submitTelemetryBox"); + Assert.ok( + !telmetryCheckbox.disabled, + "Telemetry checkbox must be enabled if FHR is checked." + ); + Assert.ok( + Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED), + "Telemetry must be enabled if the checkbox is ticked." + ); + + // Uncheck the FHR checkbox and make sure that Telemetry checkbox gets disabled. + fhrCheckbox.click(); + + Assert.ok( + telmetryCheckbox.disabled, + "Telemetry checkbox must be disabled if FHR is unchecked." + ); + Assert.ok( + !Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED), + "Telemetry must be disabled if the checkbox is unticked." + ); + + win.close(); + finish(); +} + +function resetPreferences() { + Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled"); + Services.prefs.clearUserPref(PREF_TELEMETRY_ENABLED); +} 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..fd53b8f573 --- /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/engine1/manifest.json b/browser/components/preferences/tests/engine1/manifest.json new file mode 100644 index 0000000000..e30a128de1 --- /dev/null +++ b/browser/components/preferences/tests/engine1/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine1", + "manifest_version": 2, + "version": "1.0", + "applications": { + "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..c69472ffcb --- /dev/null +++ b/browser/components/preferences/tests/engine2/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine2", + "manifest_version": 2, + "version": "1.0", + "applications": { + "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..69d174d534 --- /dev/null +++ b/browser/components/preferences/tests/head.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.import("resource://gre/modules/Promise.jsm", this); +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +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)); + } + ); + }); +} + +/** + * Waits a specified number of miliseconds for a specified event to be + * fired on a specified element. + * + * Usage: + * let receivedEvent = waitForEvent(element, "eventName"); + * // Do some processing here that will cause the event to be fired + * // ... + * // Now yield until the Promise is fulfilled + * yield receivedEvent; + * if (receivedEvent && !(receivedEvent instanceof Error)) { + * receivedEvent.msg == "eventName"; + * // ... + * } + * + * @param aSubject the element that should receive the event + * @param aEventName the event to wait for + * @param aTimeoutMs the number of miliseconds to wait before giving up + * @returns a Promise that resolves to the received event, or to an Error + */ +function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) { + let eventDeferred = Promise.defer(); + let timeoutMs = aTimeoutMs || kDefaultWait; + let stack = new Error().stack; + let timerID = setTimeout(function wfe_canceller() { + aSubject.removeEventListener(aEventName, listener); + eventDeferred.reject(new Error(aEventName + " event timeout at " + stack)); + }, timeoutMs); + + var listener = function(aEvent) { + if (aTarget && aTarget !== aEvent.target) { + return; + } + + // stop the timeout clock and resume + clearTimeout(timerID); + eventDeferred.resolve(aEvent); + }; + + function cleanup(aEventOrError) { + // unhook listener in case of success or failure + aSubject.removeEventListener(aEventName, listener); + return aEventOrError; + } + aSubject.addEventListener(aEventName, listener); + return eventDeferred.promise.then(cleanup, cleanup); +} + +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 evaluateSearchResults( + keyword, + searchReults, + includeExperiments = false +) { + searchReults = Array.isArray(searchReults) ? searchReults : [searchReults]; + searchReults.push("header-searchResults"); + + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + searchInput.focus(); + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == keyword + ); + EventUtils.sendString(keyword); + await searchCompletedPromise; + + 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 (searchReults.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-avif", + description: "pane-experimental-description", + 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; + } +} 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..b3c7426a95 --- /dev/null +++ b/browser/components/preferences/tests/privacypane_tests_perwindow.js @@ -0,0 +1,392 @@ +// 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"); + SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.instantApply", true]], + }); + + 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..c85530b3c6 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser.ini @@ -0,0 +1,16 @@ +[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 = debug && ((os == 'mac') || (os == 'linux')) || os == "win" #Bug 1533681 +[browser_siteData2.js] +[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..d080f85e14 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_clearSiteData.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { SitePermissions } = ChromeUtils.import( + "resource:///modules/SitePermissions.jsm" +); +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +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..3790c88b9d --- /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 + OfflineAppCacheHelper.clear(); + 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 wroker 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..a39f3bd828 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData2.js @@ -0,0 +1,364 @@ +"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); +}); 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..d2a4fe9838 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData3.js @@ -0,0 +1,226 @@ +"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 host +add_task(async function test_grouping() { + let quotaUsage = 7000000; + await addTestData([ + { + 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, + }, + ]); + + 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 = "account.xyz.com"; + is(columns[0].value, expected, "Should group and list sites by host"); + + is( + columns[1].value, + "5", + "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 = [ + { + baseDomain: "xyz.com", + usage: 1024, + origin: "https://account.xyz.com", + cookies: 6, + persisted: true, + }, + { + baseDomain: "foo.com", + usage: 1024 * 2, + origin: "https://books.foo.com", + cookies: 0, + persisted: false, + }, + { + baseDomain: "bar.com", + usage: 1024 * 3, + origin: "http://cinema.bar.com", + cookies: 3, + persisted: true, + }, + ]; + + 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(), + ["cinema.bar.com", "books.foo.com", "account.xyz.com"], + "Has sorted descending by usage" + ); + + // Test sorting on the usage column + usageCol.click(); + Assert.deepEqual( + getHostOrder(), + ["account.xyz.com", "books.foo.com", "cinema.bar.com"], + "Has sorted ascending by usage" + ); + usageCol.click(); + Assert.deepEqual( + getHostOrder(), + ["cinema.bar.com", "books.foo.com", "account.xyz.com"], + "Has sorted descending by usage" + ); + + // Test sorting on the host column + hostCol.click(); + Assert.deepEqual( + getHostOrder(), + ["cinema.bar.com", "books.foo.com", "account.xyz.com"], + "Has sorted ascending by base domain" + ); + hostCol.click(); + Assert.deepEqual( + getHostOrder(), + ["account.xyz.com", "books.foo.com", "cinema.bar.com"], + "Has sorted descending by base domain" + ); + + // Test sorting on the cookies column + cookiesCol.click(); + Assert.deepEqual( + getHostOrder(), + ["books.foo.com", "cinema.bar.com", "account.xyz.com"], + "Has sorted ascending by cookies" + ); + cookiesCol.click(); + Assert.deepEqual( + getHostOrder(), + ["account.xyz.com", "cinema.bar.com", "books.foo.com"], + "Has sorted descending by cookies" + ); + + 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..e9999ee350 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js @@ -0,0 +1,104 @@ +"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, + }, + ]); + + 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"); + 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..872ed93c10 --- /dev/null +++ b/browser/components/preferences/tests/siteData/head.js @@ -0,0 +1,285 @@ +/* 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"; + +const { DownloadUtils } = ChromeUtils.import( + "resource://gre/modules/DownloadUtils.jsm" +); +const { SiteDataManager } = ChromeUtils.import( + "resource:///modules/SiteDataManager.jsm" +); +const { OfflineAppCacheHelper } = ChromeUtils.import( + "resource://gre/modules/offlineAppCache.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "SiteDataTestUtils", + "resource://testing-common/SiteDataTestUtils.jsm" +); + +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"); +} + +async function addTestData(data) { + let hosts = []; + + 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(site.origin, Cu.now()); + } + + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + site.origin + ); + hosts.push(principal.host); + } + + return 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 BrowserTestUtils.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 BrowserTestUtils.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 BrowserTestUtils.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> |