summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/preferences')
-rw-r--r--browser/components/preferences/containers.inc.xhtml42
-rw-r--r--browser/components/preferences/containers.js151
-rw-r--r--browser/components/preferences/dialogs/addEngine.css24
-rw-r--r--browser/components/preferences/dialogs/addEngine.js69
-rw-r--r--browser/components/preferences/dialogs/addEngine.xhtml72
-rw-r--r--browser/components/preferences/dialogs/applicationManager.js129
-rw-r--r--browser/components/preferences/dialogs/applicationManager.xhtml68
-rw-r--r--browser/components/preferences/dialogs/blocklists.js175
-rw-r--r--browser/components/preferences/dialogs/blocklists.xhtml80
-rw-r--r--browser/components/preferences/dialogs/browserLanguages.js731
-rw-r--r--browser/components/preferences/dialogs/browserLanguages.xhtml84
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.css20
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.js96
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.xhtml70
-rw-r--r--browser/components/preferences/dialogs/colors.js19
-rw-r--r--browser/components/preferences/dialogs/colors.xhtml140
-rw-r--r--browser/components/preferences/dialogs/connection.js381
-rw-r--r--browser/components/preferences/dialogs/connection.xhtml244
-rw-r--r--browser/components/preferences/dialogs/containers.js167
-rw-r--r--browser/components/preferences/dialogs/containers.xhtml72
-rw-r--r--browser/components/preferences/dialogs/dohExceptions.js287
-rw-r--r--browser/components/preferences/dialogs/dohExceptions.xhtml104
-rw-r--r--browser/components/preferences/dialogs/fonts.js173
-rw-r--r--browser/components/preferences/dialogs/fonts.xhtml251
-rw-r--r--browser/components/preferences/dialogs/handlers.css21
-rw-r--r--browser/components/preferences/dialogs/jar.mn49
-rw-r--r--browser/components/preferences/dialogs/languages.js384
-rw-r--r--browser/components/preferences/dialogs/languages.xhtml104
-rw-r--r--browser/components/preferences/dialogs/moz.build13
-rw-r--r--browser/components/preferences/dialogs/permissions.js645
-rw-r--r--browser/components/preferences/dialogs/permissions.xhtml134
-rw-r--r--browser/components/preferences/dialogs/sanitize.js38
-rw-r--r--browser/components/preferences/dialogs/sanitize.xhtml91
-rw-r--r--browser/components/preferences/dialogs/selectBookmark.js119
-rw-r--r--browser/components/preferences/dialogs/selectBookmark.xhtml55
-rw-r--r--browser/components/preferences/dialogs/siteDataRemoveSelected.js56
-rw-r--r--browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml48
-rw-r--r--browser/components/preferences/dialogs/siteDataSettings.js335
-rw-r--r--browser/components/preferences/dialogs/siteDataSettings.xhtml86
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.css70
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.js679
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.xhtml115
-rw-r--r--browser/components/preferences/dialogs/syncChooseWhatToSync.js60
-rw-r--r--browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml88
-rw-r--r--browser/components/preferences/dialogs/translationExceptions.js256
-rw-r--r--browser/components/preferences/dialogs/translationExceptions.xhtml127
-rw-r--r--browser/components/preferences/dialogs/translations.js465
-rw-r--r--browser/components/preferences/dialogs/translations.xhtml158
-rw-r--r--browser/components/preferences/experimental.inc.xhtml37
-rw-r--r--browser/components/preferences/experimental.js163
-rw-r--r--browser/components/preferences/extensionControlled.js309
-rw-r--r--browser/components/preferences/findInPage.js772
-rw-r--r--browser/components/preferences/fxaPairDevice.js144
-rw-r--r--browser/components/preferences/fxaPairDevice.xhtml75
-rw-r--r--browser/components/preferences/home.inc.xhtml92
-rw-r--r--browser/components/preferences/home.js694
-rw-r--r--browser/components/preferences/jar.mn24
-rw-r--r--browser/components/preferences/main.inc.xhtml837
-rw-r--r--browser/components/preferences/main.js4258
-rw-r--r--browser/components/preferences/more-from-mozilla-qr-code-simple-cn.svg4
-rw-r--r--browser/components/preferences/more-from-mozilla-qr-code-simple.svg4
-rw-r--r--browser/components/preferences/moreFromMozilla.inc.xhtml44
-rw-r--r--browser/components/preferences/moreFromMozilla.js271
-rw-r--r--browser/components/preferences/moz.build23
-rw-r--r--browser/components/preferences/preferences.js661
-rw-r--r--browser/components/preferences/preferences.xhtml244
-rw-r--r--browser/components/preferences/privacy.inc.xhtml1380
-rw-r--r--browser/components/preferences/privacy.js3358
-rw-r--r--browser/components/preferences/search.inc.xhtml118
-rw-r--r--browser/components/preferences/search.js1100
-rw-r--r--browser/components/preferences/searchResults.inc.xhtml25
-rw-r--r--browser/components/preferences/sync.inc.xhtml245
-rw-r--r--browser/components/preferences/sync.js564
-rw-r--r--browser/components/preferences/tests/addons/pl-dictionary.xpibin0 -> 793 bytes
-rw-r--r--browser/components/preferences/tests/addons/set_homepage.xpibin0 -> 5156 bytes
-rw-r--r--browser/components/preferences/tests/addons/set_newtab.xpibin0 -> 5210 bytes
-rw-r--r--browser/components/preferences/tests/browser.ini152
-rw-r--r--browser/components/preferences/tests/browser_advanced_update.js185
-rw-r--r--browser/components/preferences/tests/browser_application_xml_handle_internally.js49
-rw-r--r--browser/components/preferences/tests/browser_applications_selection.js403
-rw-r--r--browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js235
-rw-r--r--browser/components/preferences/tests/browser_browser_languages_subdialog.js1058
-rw-r--r--browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js30
-rw-r--r--browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js163
-rw-r--r--browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js116
-rw-r--r--browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml26
-rw-r--r--browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js24
-rw-r--r--browser/components/preferences/tests/browser_bug1579418.js55
-rw-r--r--browser/components/preferences/tests/browser_bug410900.js47
-rw-r--r--browser/components/preferences/tests/browser_bug731866.js95
-rw-r--r--browser/components/preferences/tests/browser_bug795764_cachedisabled.js62
-rw-r--r--browser/components/preferences/tests/browser_cert_export.js161
-rw-r--r--browser/components/preferences/tests/browser_change_app_handler.js155
-rw-r--r--browser/components/preferences/tests/browser_checkspelling.js34
-rw-r--r--browser/components/preferences/tests/browser_connection.js145
-rw-r--r--browser/components/preferences/tests/browser_connection_bug1445991.js31
-rw-r--r--browser/components/preferences/tests/browser_connection_bug1505330.js31
-rw-r--r--browser/components/preferences/tests/browser_connection_bug388287.js124
-rw-r--r--browser/components/preferences/tests/browser_containers_name_input.js72
-rw-r--r--browser/components/preferences/tests/browser_contentblocking.js1382
-rw-r--r--browser/components/preferences/tests/browser_contentblocking_categories.js487
-rw-r--r--browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js148
-rw-r--r--browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js299
-rw-r--r--browser/components/preferences/tests/browser_cookies_exceptions.js568
-rw-r--r--browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js185
-rw-r--r--browser/components/preferences/tests/browser_engines.js141
-rw-r--r--browser/components/preferences/tests/browser_etp_exceptions_dialog.js96
-rw-r--r--browser/components/preferences/tests/browser_experimental_features.js74
-rw-r--r--browser/components/preferences/tests/browser_experimental_features_filter.js183
-rw-r--r--browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js86
-rw-r--r--browser/components/preferences/tests/browser_experimental_features_resetall.js112
-rw-r--r--browser/components/preferences/tests/browser_extension_controlled.js1447
-rw-r--r--browser/components/preferences/tests/browser_filetype_dialog.js189
-rw-r--r--browser/components/preferences/tests/browser_fluent.js40
-rw-r--r--browser/components/preferences/tests/browser_homepage_default.js31
-rw-r--r--browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js33
-rw-r--r--browser/components/preferences/tests/browser_homepages_use_bookmark.js94
-rw-r--r--browser/components/preferences/tests/browser_hometab_restore_defaults.js220
-rw-r--r--browser/components/preferences/tests/browser_https_only_exceptions.js279
-rw-r--r--browser/components/preferences/tests/browser_https_only_section.js74
-rw-r--r--browser/components/preferences/tests/browser_ignore_invalid_capability.js40
-rw-r--r--browser/components/preferences/tests/browser_languages_subdialog.js139
-rw-r--r--browser/components/preferences/tests/browser_layersacceleration.js36
-rw-r--r--browser/components/preferences/tests/browser_localSearchShortcuts.js309
-rw-r--r--browser/components/preferences/tests/browser_moreFromMozilla.js380
-rw-r--r--browser/components/preferences/tests/browser_moreFromMozilla_locales.js331
-rw-r--r--browser/components/preferences/tests/browser_newtab_menu.js38
-rw-r--r--browser/components/preferences/tests/browser_notifications_do_not_disturb.js57
-rw-r--r--browser/components/preferences/tests/browser_open_download_preferences.js288
-rw-r--r--browser/components/preferences/tests/browser_open_migration_wizard.js54
-rw-r--r--browser/components/preferences/tests/browser_password_management.js43
-rw-r--r--browser/components/preferences/tests/browser_pdf_disabled.js49
-rw-r--r--browser/components/preferences/tests/browser_performance.js300
-rw-r--r--browser/components/preferences/tests/browser_performance_content_process_limit.js52
-rw-r--r--browser/components/preferences/tests/browser_performance_e10srollout.js164
-rw-r--r--browser/components/preferences/tests/browser_performance_non_e10s.js210
-rw-r--r--browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js127
-rw-r--r--browser/components/preferences/tests/browser_permissions_dialog.js642
-rw-r--r--browser/components/preferences/tests/browser_permissions_dialog_default_perm.js145
-rw-r--r--browser/components/preferences/tests/browser_permissions_urlFieldHidden.js38
-rw-r--r--browser/components/preferences/tests/browser_primaryPassword.js130
-rw-r--r--browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js210
-rw-r--r--browser/components/preferences/tests/browser_privacy_dnsoverhttps.js844
-rw-r--r--browser/components/preferences/tests/browser_privacy_firefoxSuggest.js855
-rw-r--r--browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js199
-rw-r--r--browser/components/preferences/tests/browser_privacy_quickactions.js110
-rw-r--r--browser/components/preferences/tests/browser_privacy_relayIntegration.js251
-rw-r--r--browser/components/preferences/tests/browser_privacy_segmentation_pref.js131
-rw-r--r--browser/components/preferences/tests/browser_privacy_syncDataClearing.js287
-rw-r--r--browser/components/preferences/tests/browser_privacypane_2.js19
-rw-r--r--browser/components/preferences/tests/browser_privacypane_3.js21
-rw-r--r--browser/components/preferences/tests/browser_proxy_backup.js84
-rw-r--r--browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js47
-rw-r--r--browser/components/preferences/tests/browser_searchChangedEngine.js90
-rw-r--r--browser/components/preferences/tests/browser_searchDefaultEngine.js372
-rw-r--r--browser/components/preferences/tests/browser_searchFindMoreLink.js36
-rw-r--r--browser/components/preferences/tests/browser_searchRestoreDefaults.js259
-rw-r--r--browser/components/preferences/tests/browser_searchScroll.js66
-rw-r--r--browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js240
-rw-r--r--browser/components/preferences/tests/browser_search_no_results_change_category.js44
-rw-r--r--browser/components/preferences/tests/browser_search_searchTerms.js201
-rw-r--r--browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js39
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js48
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js36
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js35
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js39
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js46
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js35
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js34
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js45
-rw-r--r--browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js45
-rw-r--r--browser/components/preferences/tests/browser_search_within_preferences_1.js344
-rw-r--r--browser/components/preferences/tests/browser_search_within_preferences_2.js180
-rw-r--r--browser/components/preferences/tests/browser_search_within_preferences_command.js45
-rw-r--r--browser/components/preferences/tests/browser_searchsuggestions.js128
-rw-r--r--browser/components/preferences/tests/browser_security-1.js106
-rw-r--r--browser/components/preferences/tests/browser_security-2.js177
-rw-r--r--browser/components/preferences/tests/browser_security-3.js130
-rw-r--r--browser/components/preferences/tests/browser_site_login_exceptions.js101
-rw-r--r--browser/components/preferences/tests/browser_site_login_exceptions_policy.js65
-rw-r--r--browser/components/preferences/tests/browser_spotlight.js72
-rw-r--r--browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js124
-rw-r--r--browser/components/preferences/tests/browser_statePartitioning_strings.js79
-rw-r--r--browser/components/preferences/tests/browser_subdialogs.js639
-rw-r--r--browser/components/preferences/tests/browser_sync_chooseWhatToSync.js178
-rw-r--r--browser/components/preferences/tests/browser_sync_disabled.js26
-rw-r--r--browser/components/preferences/tests/browser_sync_pairing.js149
-rw-r--r--browser/components/preferences/tests/browser_warning_permanent_private_browsing.js57
-rw-r--r--browser/components/preferences/tests/empty_pdf_file.pdf0
-rw-r--r--browser/components/preferences/tests/engine1/manifest.json27
-rw-r--r--browser/components/preferences/tests/engine2/manifest.json27
-rw-r--r--browser/components/preferences/tests/head.js334
-rw-r--r--browser/components/preferences/tests/privacypane_tests_perwindow.js388
-rw-r--r--browser/components/preferences/tests/siteData/browser.ini22
-rw-r--r--browser/components/preferences/tests/siteData/browser_clearSiteData.js219
-rw-r--r--browser/components/preferences/tests/siteData/browser_siteData.js400
-rw-r--r--browser/components/preferences/tests/siteData/browser_siteData2.js475
-rw-r--r--browser/components/preferences/tests/siteData/browser_siteData3.js327
-rw-r--r--browser/components/preferences/tests/siteData/browser_siteData_multi_select.js119
-rw-r--r--browser/components/preferences/tests/siteData/head.js280
-rw-r--r--browser/components/preferences/tests/siteData/offline/manifest.appcache3
-rw-r--r--browser/components/preferences/tests/siteData/offline/offline.html13
-rw-r--r--browser/components/preferences/tests/siteData/service_worker_test.html19
-rw-r--r--browser/components/preferences/tests/siteData/service_worker_test.js1
-rw-r--r--browser/components/preferences/tests/siteData/site_data_test.html29
-rw-r--r--browser/components/preferences/tests/subdialog.xhtml29
-rw-r--r--browser/components/preferences/tests/subdialog2.xhtml29
-rw-r--r--browser/components/preferences/web-appearance-dark.svg17
-rw-r--r--browser/components/preferences/web-appearance-light.svg17
209 files changed, 46627 insertions, 0 deletions
diff --git a/browser/components/preferences/containers.inc.xhtml b/browser/components/preferences/containers.inc.xhtml
new file mode 100644
index 0000000000..7e2a7172dd
--- /dev/null
+++ b/browser/components/preferences/containers.inc.xhtml
@@ -0,0 +1,42 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!-- Containers panel -->
+
+<script src="chrome://browser/content/preferences/containers.js"/>
+
+<hbox hidden="true"
+ class="container-header-links"
+ data-category="paneContainers">
+ <button id="backContainersButton" class="back-button" data-l10n-id="containers-back-button2"/>
+</hbox>
+
+<hbox id="header-containers"
+ class="header"
+ hidden="true"
+ data-category="paneContainers">
+ <html:h1 data-l10n-id="containers-header"/>
+</hbox>
+
+<!-- Containers -->
+<groupbox id="browserContainersGroupPane" data-category="paneContainers" hidden="true"
+ data-hidden-from-search="true" data-subpanel="true">
+ <vbox id="browserContainersbox">
+ <richlistbox id="containersView"/>
+ </vbox>
+ <vbox>
+ <hbox flex="1">
+ <button id="containersAdd"
+ is="highlightable-button"
+ data-l10n-id="containers-add-button"/>
+ </hbox>
+ </vbox>
+ <vbox>
+ <hbox flex="1">
+ <checkbox id="containersNewTabCheck"
+ data-l10n-id="containers-new-tab-check"
+ preference="privacy.userContext.newTabContainerOnLeftClick.enabled"/>
+ </hbox>
+ </vbox>
+</groupbox>
diff --git a/browser/components/preferences/containers.js b/browser/components/preferences/containers.js
new file mode 100644
index 0000000000..80d1ec7cc3
--- /dev/null
+++ b/browser/components/preferences/containers.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from preferences.js */
+
+const defaultContainerIcon = "fingerprint";
+const defaultContainerColor = "blue";
+
+let gContainersPane = {
+ init() {
+ this._list = document.getElementById("containersView");
+
+ document
+ .getElementById("backContainersButton")
+ .addEventListener("command", function () {
+ gotoPref("general");
+ });
+
+ document
+ .getElementById("containersAdd")
+ .addEventListener("command", function () {
+ gContainersPane.onAddButtonCommand();
+ });
+
+ this._rebuildView();
+ },
+
+ _rebuildView() {
+ const containers = ContextualIdentityService.getPublicIdentities();
+ while (this._list.firstChild) {
+ this._list.firstChild.remove();
+ }
+ for (let container of containers) {
+ let item = document.createXULElement("richlistitem");
+
+ let outer = document.createXULElement("hbox");
+ outer.setAttribute("flex", 1);
+ outer.setAttribute("align", "center");
+ item.appendChild(outer);
+
+ let userContextIcon = document.createXULElement("hbox");
+ userContextIcon.className = "userContext-icon";
+ userContextIcon.classList.add("userContext-icon-inprefs");
+ userContextIcon.classList.add("identity-icon-" + container.icon);
+ userContextIcon.classList.add("identity-color-" + container.color);
+ outer.appendChild(userContextIcon);
+
+ let label = document.createXULElement("label");
+ label.className = "userContext-label-inprefs";
+ label.setAttribute("flex", 1);
+ let containerName = ContextualIdentityService.getUserContextLabel(
+ container.userContextId
+ );
+ label.textContent = containerName;
+ label.setAttribute("tooltiptext", containerName);
+ outer.appendChild(label);
+
+ let containerButtons = document.createXULElement("hbox");
+ containerButtons.className = "container-buttons";
+ item.appendChild(containerButtons);
+
+ let prefsButton = document.createXULElement("button");
+ prefsButton.addEventListener("command", function (event) {
+ gContainersPane.onPreferenceCommand(event.originalTarget);
+ });
+ prefsButton.setAttribute("value", container.userContextId);
+ document.l10n.setAttributes(prefsButton, "containers-settings-button");
+ containerButtons.appendChild(prefsButton);
+
+ let removeButton = document.createXULElement("button");
+ removeButton.addEventListener("command", function (event) {
+ gContainersPane.onRemoveCommand(event.originalTarget);
+ });
+ removeButton.setAttribute("value", container.userContextId);
+ document.l10n.setAttributes(removeButton, "containers-remove-button");
+ containerButtons.appendChild(removeButton);
+
+ this._list.appendChild(item);
+ }
+ },
+
+ async onRemoveCommand(button) {
+ let userContextId = parseInt(button.getAttribute("value"), 10);
+
+ let count = ContextualIdentityService.countContainerTabs(userContextId);
+ if (count > 0) {
+ let [title, message, okButton, cancelButton] =
+ await document.l10n.formatValues([
+ { id: "containers-remove-alert-title" },
+ { id: "containers-remove-alert-msg", args: { count } },
+ { id: "containers-remove-ok-button" },
+ { id: "containers-remove-cancel-button" },
+ ]);
+
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1;
+
+ let rv = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ okButton,
+ cancelButton,
+ null,
+ null,
+ {}
+ );
+ if (rv != 0) {
+ return;
+ }
+
+ await ContextualIdentityService.closeContainerTabs(userContextId);
+ }
+
+ ContextualIdentityService.remove(userContextId);
+ this._rebuildView();
+ },
+
+ onPreferenceCommand(button) {
+ this.openPreferenceDialog(button.getAttribute("value"));
+ },
+
+ onAddButtonCommand(button) {
+ this.openPreferenceDialog(null);
+ },
+
+ openPreferenceDialog(userContextId) {
+ let identity = {
+ name: "",
+ icon: defaultContainerIcon,
+ color: defaultContainerColor,
+ };
+ if (userContextId) {
+ identity =
+ ContextualIdentityService.getPublicIdentityFromId(userContextId);
+ identity.name = ContextualIdentityService.getUserContextLabel(
+ identity.userContextId
+ );
+ }
+
+ const params = { userContextId, identity };
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/containers.xhtml",
+ undefined,
+ params
+ );
+ },
+};
diff --git a/browser/components/preferences/dialogs/addEngine.css b/browser/components/preferences/dialogs/addEngine.css
new file mode 100644
index 0000000000..450e07f65f
--- /dev/null
+++ b/browser/components/preferences/dialogs/addEngine.css
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+input {
+ flex: 1;
+}
+
+hbox {
+ width: 100%;
+}
+
+#engineNameLabel,
+#engineUrlLabel,
+#engineAliasLabel {
+ /* Align the labels with the inputs */
+ margin-inline-start: 4px;
+}
+
+#engineUrl {
+ /* Full URLs should always be displayed as LTR */
+ direction: ltr;
+ text-align: match-parent;
+}
diff --git a/browser/components/preferences/dialogs/addEngine.js b/browser/components/preferences/dialogs/addEngine.js
new file mode 100644
index 0000000000..1faf8622b3
--- /dev/null
+++ b/browser/components/preferences/dialogs/addEngine.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../main.js */
+
+let gAddEngineDialog = {
+ _form: null,
+ _name: null,
+ _alias: null,
+
+ onLoad() {
+ document.mozSubdialogReady = this.init();
+ },
+
+ async init() {
+ this._dialog = document.querySelector("dialog");
+ this._form = document.getElementById("addEngineForm");
+ this._name = document.getElementById("engineName");
+ this._alias = document.getElementById("engineAlias");
+
+ this._name.addEventListener("input", this.onNameInput.bind(this));
+ this._alias.addEventListener("input", this.onAliasInput.bind(this));
+ this._form.addEventListener("input", this.onFormInput.bind(this));
+
+ document.addEventListener("dialogaccept", this.onAddEngine.bind(this));
+ },
+
+ async onAddEngine(event) {
+ let url = document
+ .getElementById("engineUrl")
+ .value.replace(/%s/, "{searchTerms}");
+ await Services.search.wrappedJSObject.addUserEngine(
+ this._name.value,
+ url,
+ this._alias.value
+ );
+ },
+
+ async onNameInput() {
+ if (this._name.value) {
+ let engine = Services.search.getEngineByName(this._name.value);
+ let validity = engine
+ ? document.getElementById("engineNameExists").textContent
+ : "";
+ this._name.setCustomValidity(validity);
+ }
+ },
+
+ async onAliasInput() {
+ let validity = "";
+ if (this._alias.value) {
+ let engine = await Services.search.getEngineByAlias(this._alias.value);
+ if (engine) {
+ engine = document.getElementById("engineAliasExists").textContent;
+ }
+ }
+ this._alias.setCustomValidity(validity);
+ },
+
+ async onFormInput() {
+ this._dialog.setAttribute(
+ "buttondisabledaccept",
+ !this._form.checkValidity()
+ );
+ },
+};
+
+window.addEventListener("load", () => gAddEngineDialog.onLoad());
diff --git a/browser/components/preferences/dialogs/addEngine.xhtml b/browser/components/preferences/dialogs/addEngine.xhtml
new file mode 100644
index 0000000000..780cda9010
--- /dev/null
+++ b/browser/components/preferences/dialogs/addEngine.xhtml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/addEngine.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="add-engine-window2"
+ data-l10n-attrs="title, style"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ buttondisabledaccept="true"
+ data-l10n-id="add-engine-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/addEngine.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/addEngine.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://browser/content/utilityOverlay.js" />
+
+ <separator class="thin" />
+
+ <html:form id="addEngineForm">
+ <html:span
+ id="engineNameExists"
+ hidden="hidden"
+ data-l10n-id="engine-name-exists"
+ />
+ <html:label
+ id="engineNameLabel"
+ for="engineName"
+ data-l10n-id="add-engine-name"
+ />
+ <hbox>
+ <html:input id="engineName" type="text" required="required" />
+ </hbox>
+
+ <html:label
+ id="engineUrlLabel"
+ for="engineUrl"
+ data-l10n-id="add-engine-url"
+ />
+ <hbox>
+ <html:input id="engineUrl" type="url" required="required" />
+ </hbox>
+
+ <html:span
+ id="engineAliasExists"
+ hidden="hidden"
+ data-l10n-id="engine-alias-exists"
+ />
+ <html:label
+ id="engineAliasLabel"
+ for="engineAlias"
+ data-l10n-id="add-engine-alias"
+ />
+ <hbox>
+ <html:input id="engineAlias" type="text" />
+ </hbox>
+ </html:form>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/applicationManager.js b/browser/components/preferences/dialogs/applicationManager.js
new file mode 100644
index 0000000000..0c2105cbe4
--- /dev/null
+++ b/browser/components/preferences/dialogs/applicationManager.js
@@ -0,0 +1,129 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/* import-globals-from ../main.js */
+
+var gAppManagerDialog = {
+ _removed: [],
+
+ onLoad() {
+ document.mozSubdialogReady = this.init();
+ },
+
+ async init() {
+ this.handlerInfo = window.arguments[0];
+
+ document.addEventListener("dialogaccept", function () {
+ gAppManagerDialog.onOK();
+ });
+
+ let gMainPane = window.parent.gMainPane;
+
+ const appDescElem = document.getElementById("appDescription");
+ if (this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ let { typeDescription } = this.handlerInfo;
+ let typeStr;
+ if (typeDescription.id) {
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+ typeStr = await document.l10n.formatValue(
+ typeDescription.id,
+ typeDescription.args
+ );
+ } else {
+ typeStr = typeDescription.raw;
+ }
+ document.l10n.setAttributes(appDescElem, "app-manager-handle-file", {
+ type: typeStr,
+ });
+ } else {
+ document.l10n.setAttributes(appDescElem, "app-manager-handle-protocol", {
+ type: this.handlerInfo.typeDescription.raw,
+ });
+ }
+
+ let list = document.getElementById("appList");
+ let listFragment = document.createDocumentFragment();
+ for (let app of this.handlerInfo.possibleApplicationHandlers.enumerate()) {
+ if (!gMainPane.isValidHandlerApp(app)) {
+ continue;
+ }
+
+ let item = document.createXULElement("richlistitem");
+ listFragment.append(item);
+ item.app = app;
+
+ let image = document.createXULElement("image");
+ image.setAttribute("src", gMainPane._getIconURLForHandlerApp(app));
+ item.appendChild(image);
+
+ let label = document.createXULElement("label");
+ label.setAttribute("value", app.name);
+ item.appendChild(label);
+ }
+ list.append(listFragment);
+
+ // Triggers onSelect which populates label
+ list.selectedIndex = 0;
+
+ // We want to block on those elements being localized because the
+ // result will impact the size of the subdialog.
+ await document.l10n.translateElements([
+ appDescElem,
+ document.getElementById("appType"),
+ ]);
+ },
+
+ onOK: function appManager_onOK() {
+ if (this._removed.length) {
+ for (var i = 0; i < this._removed.length; ++i) {
+ this.handlerInfo.removePossibleApplicationHandler(this._removed[i]);
+ }
+
+ this.handlerInfo.store();
+ }
+ },
+
+ remove: function appManager_remove() {
+ var list = document.getElementById("appList");
+ this._removed.push(list.selectedItem.app);
+ var index = list.selectedIndex;
+ var element = list.selectedItem;
+ list.removeItemFromSelection(element);
+ element.remove();
+ if (list.itemCount == 0) {
+ // The list is now empty, make the bottom part disappear
+ document.getElementById("appDetails").hidden = true;
+ } else {
+ // Select the item at the same index, if we removed the last
+ // item of the list, select the previous item
+ if (index == list.itemCount) {
+ --index;
+ }
+ list.selectedIndex = index;
+ }
+ },
+
+ onSelect: function appManager_onSelect() {
+ var list = document.getElementById("appList");
+ if (!list.selectedItem) {
+ document.getElementById("remove").disabled = true;
+ return;
+ }
+ document.getElementById("remove").disabled = false;
+ var app = list.selectedItem.app;
+ var address = "";
+ if (app instanceof Ci.nsILocalHandlerApp) {
+ address = app.executable.path;
+ } else if (app instanceof Ci.nsIWebHandlerApp) {
+ address = app.uriTemplate;
+ }
+ document.getElementById("appLocation").value = address;
+ const l10nId =
+ app instanceof Ci.nsILocalHandlerApp
+ ? "app-manager-local-app-info"
+ : "app-manager-web-app-info";
+ const appTypeElem = document.getElementById("appType");
+ document.l10n.setAttributes(appTypeElem, l10nId);
+ },
+};
diff --git a/browser/components/preferences/dialogs/applicationManager.xhtml b/browser/components/preferences/dialogs/applicationManager.xhtml
new file mode 100644
index 0000000000..889307d88c
--- /dev/null
+++ b/browser/components/preferences/dialogs/applicationManager.xhtml
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gAppManagerDialog.onLoad();"
+ data-l10n-id="app-manager-window2"
+ data-l10n-attrs="title, style"
+>
+ <dialog id="appManager" buttons="accept,cancel">
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/applicationManager.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/applicationManager.js" />
+
+ <commandset id="appManagerCommandSet">
+ <command
+ id="cmd_remove"
+ oncommand="gAppManagerDialog.remove();"
+ disabled="true"
+ />
+ </commandset>
+
+ <keyset id="appManagerKeyset">
+ <key id="delete" keycode="VK_DELETE" command="cmd_remove" />
+ </keyset>
+
+ <description id="appDescription" />
+ <separator class="thin" />
+ <hbox flex="1">
+ <richlistbox
+ id="appList"
+ onselect="gAppManagerDialog.onSelect();"
+ flex="1"
+ />
+ <vbox>
+ <button
+ id="remove"
+ data-l10n-id="app-manager-remove"
+ command="cmd_remove"
+ />
+ <spacer flex="1" />
+ </vbox>
+ </hbox>
+ <vbox id="appDetails">
+ <separator class="thin" />
+ <label id="appType" />
+ <html:input
+ type="text"
+ id="appLocation"
+ readonly="readonly"
+ style="margin-inline: 0"
+ />
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/blocklists.js b/browser/components/preferences/dialogs/blocklists.js
new file mode 100644
index 0000000000..c28ee09f96
--- /dev/null
+++ b/browser/components/preferences/dialogs/blocklists.js
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const BASE_LIST_ID = "base";
+const CONTENT_LIST_ID = "content";
+const TRACK_SUFFIX = "-track-digest256";
+const TRACKING_TABLE_PREF = "urlclassifier.trackingTable";
+const LISTS_PREF_BRANCH = "browser.safebrowsing.provider.mozilla.lists.";
+
+var gBlocklistManager = {
+ _type: "",
+ _blockLists: [],
+ _tree: null,
+
+ _view: {
+ _rowCount: 0,
+ get rowCount() {
+ return this._rowCount;
+ },
+ getCellText(row, column) {
+ if (column.id == "listCol") {
+ let list = gBlocklistManager._blockLists[row];
+ return list.name;
+ }
+ return "";
+ },
+
+ isSeparator(index) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(index) {
+ return false;
+ },
+ setTree(tree) {},
+ getImageSrc(row, column) {},
+ getCellValue(row, column) {
+ if (column.id == "selectionCol") {
+ return gBlocklistManager._blockLists[row].selected;
+ }
+ return undefined;
+ },
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ if (column.id == "selectionCol") {
+ return "checkmark";
+ }
+
+ return "";
+ },
+ },
+
+ onLoad() {
+ this.init();
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+ },
+
+ init() {
+ if (this._type) {
+ // reusing an open dialog, clear the old observer
+ this.uninit();
+ }
+
+ this._type = "tracking";
+
+ this._loadBlockLists();
+ },
+
+ uninit() {},
+
+ onListSelected() {
+ for (let list of this._blockLists) {
+ list.selected = false;
+ }
+ this._blockLists[this._tree.currentIndex].selected = true;
+
+ this._updateTree();
+ },
+
+ onApplyChanges() {
+ let activeList = this._getActiveList();
+ let selected = null;
+ for (let list of this._blockLists) {
+ if (list.selected) {
+ selected = list;
+ break;
+ }
+ }
+
+ if (activeList !== selected.id) {
+ let trackingTable = Services.prefs.getCharPref(TRACKING_TABLE_PREF);
+ if (selected.id != CONTENT_LIST_ID) {
+ trackingTable = trackingTable.replace(
+ "," + CONTENT_LIST_ID + TRACK_SUFFIX,
+ ""
+ );
+ } else {
+ trackingTable += "," + CONTENT_LIST_ID + TRACK_SUFFIX;
+ }
+ Services.prefs.setCharPref(TRACKING_TABLE_PREF, trackingTable);
+
+ // Force an update after changing the tracking protection table.
+ let listmanager = Cc[
+ "@mozilla.org/url-classifier/listmanager;1"
+ ].getService(Ci.nsIUrlListManager);
+ if (listmanager) {
+ listmanager.forceUpdates(trackingTable);
+ }
+ }
+ },
+
+ async _loadBlockLists() {
+ this._blockLists = [];
+
+ // Load blocklists into a table.
+ let branch = Services.prefs.getBranch(LISTS_PREF_BRANCH);
+ let itemArray = branch.getChildList("");
+ for (let itemName of itemArray) {
+ try {
+ let list = await this._createBlockList(itemName);
+ this._blockLists.push(list);
+ } catch (e) {
+ // Ignore bogus or missing list name.
+ continue;
+ }
+ }
+
+ this._updateTree();
+ },
+
+ async _createBlockList(id) {
+ let branch = Services.prefs.getBranch(LISTS_PREF_BRANCH);
+ let l10nKey = branch.getCharPref(id);
+
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ let [listName, description] = await document.l10n.formatValues([
+ { id: `blocklist-item-${l10nKey}-listName` },
+ { id: `blocklist-item-${l10nKey}-description` },
+ ]);
+
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ let name = await document.l10n.formatValue("blocklist-item-list-template", {
+ listName,
+ description,
+ });
+
+ return {
+ id,
+ name,
+ selected: this._getActiveList() === id,
+ };
+ },
+
+ _updateTree() {
+ this._tree = document.getElementById("blocklistsTree");
+ this._view._rowCount = this._blockLists.length;
+ this._tree.view = this._view;
+ },
+
+ _getActiveList() {
+ let trackingTable = Services.prefs.getCharPref(TRACKING_TABLE_PREF);
+ return trackingTable.includes(CONTENT_LIST_ID)
+ ? CONTENT_LIST_ID
+ : BASE_LIST_ID;
+ },
+};
diff --git a/browser/components/preferences/dialogs/blocklists.xhtml b/browser/components/preferences/dialogs/blocklists.xhtml
new file mode 100644
index 0000000000..eda42ee7ae
--- /dev/null
+++ b/browser/components/preferences/dialogs/blocklists.xhtml
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="BlocklistsDialog"
+ data-l10n-id="blocklist-window2"
+ data-l10n-attrs="title, style"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gBlocklistManager.onLoad();"
+ onunload="gBlocklistManager.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="blocklist-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="browser/preferences/blocklists.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/blocklists.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="blocklist-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description
+ id="blocklistsText"
+ data-l10n-id="blocklist-description"
+ control="url"
+ >
+ <html:a
+ target="_blank"
+ class="text-link"
+ data-l10n-name="disconnect-link"
+ href="https://disconnect.me/"
+ />
+ </description>
+ <separator class="thin" />
+ <tree
+ id="blocklistsTree"
+ flex="1"
+ style="height: 18em"
+ hidecolumnpicker="true"
+ onselect="gBlocklistManager.onListSelected();"
+ >
+ <treecols>
+ <treecol
+ id="selectionCol"
+ label=""
+ style="flex: 1 auto"
+ sortable="false"
+ type="checkbox"
+ />
+ <treecol
+ id="listCol"
+ data-l10n-id="blocklist-treehead-list"
+ style="flex: 80 80 auto"
+ sortable="false"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/browserLanguages.js b/browser/components/preferences/dialogs/browserLanguages.js
new file mode 100644
index 0000000000..3dc7e3f9ff
--- /dev/null
+++ b/browser/components/preferences/dialogs/browserLanguages.js
@@ -0,0 +1,731 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+// This is exported by preferences.js but we can't import that in a subdialog.
+let { LangPackMatcher } = window.top;
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "SelectionChangedMenulist",
+ "resource:///modules/SelectionChangedMenulist.jsm"
+);
+
+document
+ .getElementById("BrowserLanguagesDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+/* This dialog provides an interface for managing what language the browser is
+ * displayed in.
+ *
+ * There is a list of "requested" locales and a list of "available" locales. The
+ * requested locales must be installed and enabled. Available locales could be
+ * installed and enabled, or fetched from the AMO language tools API.
+ *
+ * If a langpack is disabled, there is no way to determine what locale it is for and
+ * it will only be listed as available if that locale is also available on AMO and
+ * the user has opted to search for more languages.
+ */
+
+async function installFromUrl(url, hash, callback) {
+ let telemetryInfo = {
+ source: "about:preferences",
+ };
+ let install = await AddonManager.getInstallForURL(url, {
+ hash,
+ telemetryInfo,
+ });
+ if (callback) {
+ callback(install.installId.toString());
+ }
+ await install.install();
+ return install.addon;
+}
+
+async function dictionaryIdsForLocale(locale) {
+ let entries = await RemoteSettings("language-dictionaries").get({
+ filters: { id: locale },
+ });
+ if (entries.length) {
+ return entries[0].dictionaries;
+ }
+ return [];
+}
+
+class OrderedListBox {
+ constructor({
+ richlistbox,
+ upButton,
+ downButton,
+ removeButton,
+ onRemove,
+ onReorder,
+ }) {
+ this.richlistbox = richlistbox;
+ this.upButton = upButton;
+ this.downButton = downButton;
+ this.removeButton = removeButton;
+ this.onRemove = onRemove;
+ this.onReorder = onReorder;
+
+ this.items = [];
+
+ this.richlistbox.addEventListener("select", () => this.setButtonState());
+ this.upButton.addEventListener("command", () => this.moveUp());
+ this.downButton.addEventListener("command", () => this.moveDown());
+ this.removeButton.addEventListener("command", () => this.removeItem());
+ }
+
+ get selectedItem() {
+ return this.items[this.richlistbox.selectedIndex];
+ }
+
+ setButtonState() {
+ let { upButton, downButton, removeButton } = this;
+ let { selectedIndex, itemCount } = this.richlistbox;
+ upButton.disabled = selectedIndex <= 0;
+ downButton.disabled = selectedIndex == itemCount - 1;
+ removeButton.disabled = itemCount <= 1 || !this.selectedItem.canRemove;
+ }
+
+ moveUp() {
+ let { selectedIndex } = this.richlistbox;
+ if (selectedIndex == 0) {
+ return;
+ }
+ let { items } = this;
+ let selectedItem = items[selectedIndex];
+ let prevItem = items[selectedIndex - 1];
+ items[selectedIndex - 1] = items[selectedIndex];
+ items[selectedIndex] = prevItem;
+ let prevEl = document.getElementById(prevItem.id);
+ let selectedEl = document.getElementById(selectedItem.id);
+ this.richlistbox.insertBefore(selectedEl, prevEl);
+ this.richlistbox.ensureElementIsVisible(selectedEl);
+ this.setButtonState();
+
+ this.onReorder();
+ }
+
+ moveDown() {
+ let { selectedIndex } = this.richlistbox;
+ if (selectedIndex == this.items.length - 1) {
+ return;
+ }
+ let { items } = this;
+ let selectedItem = items[selectedIndex];
+ let nextItem = items[selectedIndex + 1];
+ items[selectedIndex + 1] = items[selectedIndex];
+ items[selectedIndex] = nextItem;
+ let nextEl = document.getElementById(nextItem.id);
+ let selectedEl = document.getElementById(selectedItem.id);
+ this.richlistbox.insertBefore(nextEl, selectedEl);
+ this.richlistbox.ensureElementIsVisible(selectedEl);
+ this.setButtonState();
+
+ this.onReorder();
+ }
+
+ removeItem() {
+ let { selectedIndex } = this.richlistbox;
+
+ if (selectedIndex == -1) {
+ return;
+ }
+
+ let [item] = this.items.splice(selectedIndex, 1);
+ this.richlistbox.selectedItem.remove();
+ this.richlistbox.selectedIndex = Math.min(
+ selectedIndex,
+ this.richlistbox.itemCount - 1
+ );
+ this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+ this.onRemove(item);
+ }
+
+ setItems(items) {
+ this.items = items;
+ this.populate();
+ this.setButtonState();
+ }
+
+ /**
+ * Add an item to the top of the ordered list.
+ *
+ * @param {object} item The item to insert.
+ */
+ addItem(item) {
+ this.items.unshift(item);
+ this.richlistbox.insertBefore(
+ this.createItem(item),
+ this.richlistbox.firstElementChild
+ );
+ this.richlistbox.selectedIndex = 0;
+ this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+ }
+
+ populate() {
+ this.richlistbox.textContent = "";
+
+ let frag = document.createDocumentFragment();
+ for (let item of this.items) {
+ frag.appendChild(this.createItem(item));
+ }
+ this.richlistbox.appendChild(frag);
+
+ this.richlistbox.selectedIndex = 0;
+ this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+ }
+
+ createItem({ id, label, value }) {
+ let listitem = document.createXULElement("richlistitem");
+ listitem.id = id;
+ listitem.setAttribute("value", value);
+
+ let labelEl = document.createXULElement("label");
+ labelEl.textContent = label;
+ listitem.appendChild(labelEl);
+
+ return listitem;
+ }
+}
+
+/**
+ * The sorted select list of Locales available for the app.
+ */
+class SortedItemSelectList {
+ constructor({ menulist, button, onSelect, onChange, compareFn }) {
+ /** @type {XULElement} */
+ this.menulist = menulist;
+
+ /** @type {XULElement} */
+ this.popup = menulist.menupopup;
+
+ /** @type {XULElement} */
+ this.button = button;
+
+ /** @type {(a: LocaleDisplayInfo, b: LocaleDisplayInfo) => number} */
+ this.compareFn = compareFn;
+
+ /** @type {Array<LocaleDisplayInfo>} */
+ this.items = [];
+
+ // This will register the "command" listener.
+ new SelectionChangedMenulist(this.menulist, () => {
+ button.disabled = !menulist.selectedItem;
+ if (menulist.selectedItem) {
+ onChange(this.items[menulist.selectedIndex]);
+ }
+ });
+ button.addEventListener("command", () => {
+ if (!menulist.selectedItem) {
+ return;
+ }
+
+ let [item] = this.items.splice(menulist.selectedIndex, 1);
+ menulist.selectedItem.remove();
+ menulist.setAttribute("label", menulist.getAttribute("placeholder"));
+ button.disabled = true;
+ menulist.disabled = menulist.itemCount == 0;
+ menulist.selectedIndex = -1;
+
+ onSelect(item);
+ });
+ }
+
+ /**
+ * @param {Array<LocaleDisplayInfo>} items
+ */
+ setItems(items) {
+ this.items = items.sort(this.compareFn);
+ this.populate();
+ }
+
+ populate() {
+ let { button, items, menulist, popup } = this;
+ popup.textContent = "";
+
+ let frag = document.createDocumentFragment();
+ for (let item of items) {
+ frag.appendChild(this.createItem(item));
+ }
+ popup.appendChild(frag);
+
+ menulist.setAttribute("label", menulist.getAttribute("placeholder"));
+ menulist.disabled = menulist.itemCount == 0;
+ menulist.selectedIndex = -1;
+ button.disabled = true;
+ }
+
+ /**
+ * Add an item to the list sorted by the label.
+ *
+ * @param {object} item The item to insert.
+ */
+ addItem(item) {
+ let { compareFn, items, menulist, popup } = this;
+
+ // Find the index of the item to insert before.
+ let i = items.findIndex(el => compareFn(el, item) >= 0);
+ items.splice(i, 0, item);
+ popup.insertBefore(this.createItem(item), menulist.getItemAtIndex(i));
+
+ menulist.disabled = menulist.itemCount == 0;
+ }
+
+ createItem({ label, value, className, disabled }) {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("label", label);
+ if (value) {
+ item.value = value;
+ }
+ if (className) {
+ item.classList.add(className);
+ }
+ if (disabled) {
+ item.setAttribute("disabled", "true");
+ }
+ return item;
+ }
+
+ /**
+ * Disable the inputs and set a data-l10n-id on the menulist. This can be
+ * reverted with `enableWithMessageId()`.
+ */
+ disableWithMessageId(messageId) {
+ this.menulist.setAttribute("data-l10n-id", messageId);
+ this.menulist.setAttribute(
+ "image",
+ "chrome://browser/skin/tabbrowser/tab-connecting.png"
+ );
+ this.menulist.disabled = true;
+ this.button.disabled = true;
+ }
+
+ /**
+ * Enable the inputs and set a data-l10n-id on the menulist. This can be
+ * reverted with `disableWithMessageId()`.
+ */
+ enableWithMessageId(messageId) {
+ this.menulist.setAttribute("data-l10n-id", messageId);
+ this.menulist.removeAttribute("image");
+ this.menulist.disabled = this.menulist.itemCount == 0;
+ this.button.disabled = !this.menulist.selectedItem;
+ }
+}
+
+/**
+ * @typedef LocaleDisplayInfo
+ * @type {object}
+ * @prop {string} id - A unique ID.
+ * @prop {string} label - The localized display name.
+ * @prop {string} value - The BCP 47 locale identifier or the word "search".
+ * @prop {boolean} canRemove - Locales that are part of the packaged locales cannot be
+ * removed.
+ * @prop {boolean} installed - Whether or not the locale is installed.
+ */
+
+/**
+ * @param {Array<string>} localeCodes - List of BCP 47 locale identifiers.
+ * @returns {Array<LocaleDisplayInfo>}
+ */
+async function getLocaleDisplayInfo(localeCodes) {
+ let availableLocales = new Set(await LangPackMatcher.getAvailableLocales());
+ let packagedLocales = new Set(Services.locale.packagedLocales);
+ let localeNames = Services.intl.getLocaleDisplayNames(
+ undefined,
+ localeCodes,
+ { preferNative: true }
+ );
+ return localeCodes.map((code, i) => {
+ return {
+ id: "locale-" + code,
+ label: localeNames[i],
+ value: code,
+ canRemove: !packagedLocales.has(code),
+ installed: availableLocales.has(code),
+ };
+ });
+}
+
+/**
+ * @param {LocaleDisplayInfo} a
+ * @param {LocaleDisplayInfo} b
+ * @returns {number}
+ */
+function compareItems(a, b) {
+ // Sort by installed.
+ if (a.installed != b.installed) {
+ return a.installed ? -1 : 1;
+
+ // The search label is always last.
+ } else if (a.value == "search") {
+ return 1;
+ } else if (b.value == "search") {
+ return -1;
+
+ // If both items are locales, sort by label.
+ } else if (a.value && b.value) {
+ return a.label.localeCompare(b.label);
+
+ // One of them is a label, put it first.
+ } else if (a.value) {
+ return 1;
+ }
+ return -1;
+}
+
+var gBrowserLanguagesDialog = {
+ /**
+ * The publicly readable list of selected locales. It is only set when the dialog is
+ * accepted, and can be retrieved elsewhere by directly reading the property
+ * on gBrowserLanguagesDialog.
+ *
+ * let { selected } = gBrowserLanguagesDialog;
+ *
+ * @type {null | Array<string>}
+ */
+ selected: null,
+
+ /**
+ * @type {string | null} An ID used for telemetry pings. It is unique to the current
+ * opening of the browser language.
+ */
+ _telemetryId: null,
+
+ /**
+ * @type {SortedItemSelectList}
+ */
+ _availableLocalesUI: null,
+
+ /**
+ * @type {OrderedListBox}
+ */
+ _selectedLocalesUI: null,
+
+ get downloadEnabled() {
+ // Downloading langpacks isn't always supported, check the pref.
+ return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled");
+ },
+
+ recordTelemetry(method, extra = null) {
+ Services.telemetry.recordEvent(
+ "intl.ui.browserLanguage",
+ method,
+ "dialog",
+ this._telemetryId,
+ extra
+ );
+ },
+
+ async onLoad() {
+ /**
+ * @typedef {Object} Options - Options passed in to configure the subdialog.
+ * @property {string} telemetryId,
+ * @property {Array<string>} [selectedLocalesForRestart] The optional list of
+ * previously selected locales for when a restart is required. This list is
+ * preserved between openings of the dialog.
+ * @property {boolean} search Whether the user opened this from "Search for more
+ * languages" option.
+ */
+
+ /** @type {Options} */
+ let { telemetryId, selectedLocalesForRestart, search } =
+ window.arguments[0];
+
+ this._telemetryId = telemetryId;
+
+ // This is a list of available locales that the user selected. It's more
+ // restricted than the Intl notion of `requested` as it only contains
+ // locale codes for which we have matching locales available.
+ // The first time this dialog is opened, populate with appLocalesAsBCP47.
+ let selectedLocales =
+ selectedLocalesForRestart || Services.locale.appLocalesAsBCP47;
+ let selectedLocaleSet = new Set(selectedLocales);
+ let available = await LangPackMatcher.getAvailableLocales();
+ let availableSet = new Set(available);
+
+ // Filter selectedLocales since the user may select a locale when it is
+ // available and then disable it.
+ selectedLocales = selectedLocales.filter(locale =>
+ availableSet.has(locale)
+ );
+ // Nothing in available should be in selectedSet.
+ available = available.filter(locale => !selectedLocaleSet.has(locale));
+
+ await this.initSelectedLocales(selectedLocales);
+ await this.initAvailableLocales(available, search);
+
+ this.initialized = true;
+
+ // Now the component is initialized, it's safe to accept the results.
+ document
+ .getElementById("BrowserLanguagesDialog")
+ .addEventListener("beforeaccept", () => {
+ this.selected = this._selectedLocalesUI.items.map(item => item.value);
+ });
+ },
+
+ /**
+ * @param {string[]} selectedLocales - BCP 47 locale identifiers
+ */
+ async initSelectedLocales(selectedLocales) {
+ this._selectedLocalesUI = new OrderedListBox({
+ richlistbox: document.getElementById("selectedLocales"),
+ upButton: document.getElementById("up"),
+ downButton: document.getElementById("down"),
+ removeButton: document.getElementById("remove"),
+ onRemove: item => this.selectedLocaleRemoved(item),
+ onReorder: () => this.recordTelemetry("reorder"),
+ });
+ this._selectedLocalesUI.setItems(
+ await getLocaleDisplayInfo(selectedLocales)
+ );
+ },
+
+ /**
+ * @param {Set<string>} available - The set of available BCP 47 locale identifiers.
+ * @param {boolean} search - Whether the user opened this from "Search for more
+ * languages" option.
+ */
+ async initAvailableLocales(available, search) {
+ this._availableLocalesUI = new SortedItemSelectList({
+ menulist: document.getElementById("availableLocales"),
+ button: document.getElementById("add"),
+ compareFn: compareItems,
+ onSelect: item => this.availableLanguageSelected(item),
+ onChange: item => {
+ this.hideError();
+ if (item.value == "search") {
+ // Record the search event here so we don't track the search from
+ // the main preferences pane twice.
+ this.recordTelemetry("search");
+ this.loadLocalesFromAMO();
+ }
+ },
+ });
+
+ // Populate the list with the installed locales even if the user is
+ // searching in case the download fails.
+ await this.loadLocalesFromInstalled(available);
+
+ // If the user opened this from the "Search for more languages" option,
+ // search AMO for available locales.
+ if (search) {
+ return this.loadLocalesFromAMO();
+ }
+
+ return undefined;
+ },
+
+ async loadLocalesFromAMO() {
+ if (!this.downloadEnabled) {
+ return;
+ }
+
+ // Disable the dropdown while we hit the network.
+ this._availableLocalesUI.disableWithMessageId(
+ "browser-languages-searching"
+ );
+
+ // Fetch the available langpacks from AMO.
+ let availableLangpacks;
+ try {
+ availableLangpacks = await AddonRepository.getAvailableLangpacks();
+ } catch (e) {
+ this.showError();
+ return;
+ }
+
+ // Store the available langpack info for later use.
+ this.availableLangpacks = new Map();
+ for (let { target_locale, url, hash } of availableLangpacks) {
+ this.availableLangpacks.set(target_locale, { url, hash });
+ }
+
+ // Remove the installed locales from the available ones.
+ let installedLocales = new Set(await LangPackMatcher.getAvailableLocales());
+ let notInstalledLocales = availableLangpacks
+ .filter(({ target_locale }) => !installedLocales.has(target_locale))
+ .map(lang => lang.target_locale);
+
+ // Create the rows for the remote locales.
+ let availableItems = await getLocaleDisplayInfo(notInstalledLocales);
+ availableItems.push({
+ label: await document.l10n.formatValue(
+ "browser-languages-available-label"
+ ),
+ className: "label-item",
+ disabled: true,
+ installed: false,
+ });
+
+ // Remove the search option and add the remote locales.
+ let items = this._availableLocalesUI.items;
+ items.pop();
+ items = items.concat(availableItems);
+
+ // Update the dropdown and enable it again.
+ this._availableLocalesUI.setItems(items);
+ this._availableLocalesUI.enableWithMessageId(
+ "browser-languages-select-language"
+ );
+ },
+
+ /**
+ * @param {Set<string>} available - The set of available (BCP 47) locales.
+ */
+ async loadLocalesFromInstalled(available) {
+ let items;
+ if (available.length) {
+ items = await getLocaleDisplayInfo(available);
+ items.push(await this.createInstalledLabel());
+ } else {
+ items = [];
+ }
+ if (this.downloadEnabled) {
+ items.push({
+ label: await document.l10n.formatValue("browser-languages-search"),
+ value: "search",
+ });
+ }
+ this._availableLocalesUI.setItems(items);
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async availableLanguageSelected(item) {
+ if ((await LangPackMatcher.getAvailableLocales()).includes(item.value)) {
+ this.recordTelemetry("add");
+ await this.requestLocalLanguage(item);
+ } else if (this.availableLangpacks.has(item.value)) {
+ // Telemetry is tracked in requestRemoteLanguage.
+ await this.requestRemoteLanguage(item);
+ } else {
+ this.showError();
+ }
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async requestLocalLanguage(item) {
+ this._selectedLocalesUI.addItem(item);
+ let selectedCount = this._selectedLocalesUI.items.length;
+ let availableCount = (await LangPackMatcher.getAvailableLocales()).length;
+ if (selectedCount == availableCount) {
+ // Remove the installed label, they're all installed.
+ this._availableLocalesUI.items.shift();
+ this._availableLocalesUI.setItems(this._availableLocalesUI.items);
+ }
+ // The label isn't always reset when the selected item is removed, so set it again.
+ this._availableLocalesUI.enableWithMessageId(
+ "browser-languages-select-language"
+ );
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async requestRemoteLanguage(item) {
+ this._availableLocalesUI.disableWithMessageId(
+ "browser-languages-downloading"
+ );
+
+ let { url, hash } = this.availableLangpacks.get(item.value);
+ let addon;
+
+ try {
+ addon = await installFromUrl(url, hash, installId =>
+ this.recordTelemetry("add", { installId })
+ );
+ } catch (e) {
+ this.showError();
+ return;
+ }
+
+ // If the add-on was previously installed, it might be disabled still.
+ if (addon.userDisabled) {
+ await addon.enable();
+ }
+
+ item.installed = true;
+ this._selectedLocalesUI.addItem(item);
+ this._availableLocalesUI.enableWithMessageId(
+ "browser-languages-select-language"
+ );
+
+ // This is an async task that will install the recommended dictionaries for
+ // this locale. This will fail silently at least until a management UI is
+ // added in bug 1493705.
+ this.installDictionariesForLanguage(item.value);
+ },
+
+ /**
+ * @param {string} locale The BCP 47 locale identifier
+ */
+ async installDictionariesForLanguage(locale) {
+ try {
+ let ids = await dictionaryIdsForLocale(locale);
+ let addonInfos = await AddonRepository.getAddonsByIDs(ids);
+ await Promise.all(
+ addonInfos.map(info => installFromUrl(info.sourceURI.spec))
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ showError() {
+ document.getElementById("warning-message").hidden = false;
+ this._availableLocalesUI.enableWithMessageId(
+ "browser-languages-select-language"
+ );
+
+ // The height has likely changed, find our SubDialog and tell it to resize.
+ requestAnimationFrame(() => {
+ let dialogs = window.opener.gSubDialog._dialogs;
+ let index = dialogs.findIndex(d => d._frame.contentDocument == document);
+ if (index != -1) {
+ dialogs[index].resizeDialog();
+ }
+ });
+ },
+
+ hideError() {
+ document.getElementById("warning-message").hidden = true;
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async selectedLocaleRemoved(item) {
+ this.recordTelemetry("remove");
+
+ this._availableLocalesUI.addItem(item);
+
+ // If the item we added is at the top of the list, it needs the label.
+ if (this._availableLocalesUI.items[0] == item) {
+ this._availableLocalesUI.addItem(await this.createInstalledLabel());
+ }
+ },
+
+ async createInstalledLabel() {
+ return {
+ label: await document.l10n.formatValue(
+ "browser-languages-installed-label"
+ ),
+ className: "label-item",
+ disabled: true,
+ installed: true,
+ };
+ },
+};
diff --git a/browser/components/preferences/dialogs/browserLanguages.xhtml b/browser/components/preferences/dialogs/browserLanguages.xhtml
new file mode 100644
index 0000000000..3818ccc058
--- /dev/null
+++ b/browser/components/preferences/dialogs/browserLanguages.xhtml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="browser-languages-window2"
+ data-l10n-attrs="title, style"
+ onload="gBrowserLanguagesDialog.onLoad();"
+>
+ <dialog
+ id="BrowserLanguagesDialog"
+ buttons="accept,cancel,help"
+ helpTopic="change-language"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="browser/preferences/languages.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/browserLanguages.js" />
+
+ <description data-l10n-id="browser-languages-description" />
+
+ <box class="languages-grid">
+ <richlistbox id="selectedLocales" />
+ <vbox>
+ <button
+ id="up"
+ class="action-button"
+ disabled="true"
+ data-l10n-id="languages-customize-moveup"
+ />
+ <button
+ id="down"
+ class="action-button"
+ disabled="true"
+ data-l10n-id="languages-customize-movedown"
+ />
+ <button
+ id="remove"
+ class="action-button"
+ disabled="true"
+ data-l10n-id="languages-customize-remove"
+ />
+ </vbox>
+
+ <menulist
+ id="availableLocales"
+ class="available-locales-list"
+ data-l10n-id="browser-languages-select-language"
+ data-l10n-attrs="placeholder,label"
+ >
+ <menupopup />
+ </menulist>
+ <button
+ id="add"
+ class="add-browser-language action-button"
+ data-l10n-id="languages-customize-add"
+ disabled="true"
+ />
+ </box>
+ <hbox
+ id="warning-message"
+ class="message-bar message-bar-warning"
+ hidden="true"
+ >
+ <image class="message-bar-icon" />
+ <description
+ class="message-bar-description"
+ data-l10n-id="browser-languages-error"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/clearSiteData.css b/browser/components/preferences/dialogs/clearSiteData.css
new file mode 100644
index 0000000000..5b1f5bbe2d
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.css
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.options-container {
+ background-color: var(--in-content-box-background);
+ border: 1px solid var(--in-content-box-border-color);
+ border-radius: 2px;
+ color: var(--in-content-text-color);
+ padding: 0.5em;
+}
+
+.option {
+ padding-bottom: 8px;
+}
+
+.option-description {
+ color: var(--text-color-deemphasized);
+ margin-top: -0.5em !important;
+}
diff --git a/browser/components/preferences/dialogs/clearSiteData.js b/browser/components/preferences/dialogs/clearSiteData.js
new file mode 100644
index 0000000000..eae2d5f772
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { SiteDataManager } = ChromeUtils.import(
+ "resource:///modules/SiteDataManager.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+});
+
+var gClearSiteDataDialog = {
+ _clearSiteDataCheckbox: null,
+ _clearCacheCheckbox: null,
+
+ onLoad() {
+ document.mozSubdialogReady = this.init();
+ },
+
+ async init() {
+ this._dialog = document.querySelector("dialog");
+ this._clearSiteDataCheckbox = document.getElementById("clearSiteData");
+ this._clearCacheCheckbox = document.getElementById("clearCache");
+
+ // We'll block init() on this because the result values may impact
+ // subdialog sizing.
+ await Promise.all([
+ SiteDataManager.getTotalUsage().then(bytes => {
+ let [amount, unit] = DownloadUtils.convertByteUnits(bytes);
+ document.l10n.setAttributes(
+ this._clearSiteDataCheckbox,
+ "clear-site-data-cookies-with-data",
+ { amount, unit }
+ );
+ }),
+ SiteDataManager.getCacheSize().then(bytes => {
+ let [amount, unit] = DownloadUtils.convertByteUnits(bytes);
+ document.l10n.setAttributes(
+ this._clearCacheCheckbox,
+ "clear-site-data-cache-with-data",
+ { amount, unit }
+ );
+ }),
+ ]);
+ await document.l10n.translateElements([
+ this._clearCacheCheckbox,
+ this._clearSiteDataCheckbox,
+ ]);
+
+ document.addEventListener("dialogaccept", event => this.onClear(event));
+
+ this._clearSiteDataCheckbox.addEventListener("command", e =>
+ this.onCheckboxCommand(e)
+ );
+ this._clearCacheCheckbox.addEventListener("command", e =>
+ this.onCheckboxCommand(e)
+ );
+ },
+
+ onCheckboxCommand(event) {
+ this._dialog.setAttribute(
+ "buttondisabledaccept",
+ !(this._clearSiteDataCheckbox.checked || this._clearCacheCheckbox.checked)
+ );
+ },
+
+ onClear(event) {
+ let clearSiteData = this._clearSiteDataCheckbox.checked;
+ let clearCache = this._clearCacheCheckbox.checked;
+
+ if (clearSiteData) {
+ // Ask for confirmation before clearing site data
+ if (!SiteDataManager.promptSiteDataRemoval(window)) {
+ clearSiteData = false;
+ // Prevent closing the dialog when the data removal wasn't allowed.
+ event.preventDefault();
+ }
+ }
+
+ if (clearSiteData) {
+ SiteDataManager.removeSiteData();
+ }
+ if (clearCache) {
+ SiteDataManager.removeCache();
+
+ // If we're not clearing site data, we need to tell the
+ // SiteDataManager to signal that it's updating.
+ if (!clearSiteData) {
+ SiteDataManager.updateSites();
+ }
+ }
+ },
+};
+
+window.addEventListener("load", () => gClearSiteDataDialog.onLoad());
diff --git a/browser/components/preferences/dialogs/clearSiteData.xhtml b/browser/components/preferences/dialogs/clearSiteData.xhtml
new file mode 100644
index 0000000000..574dc4fb5d
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.xhtml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/clearSiteData.css" type="text/css"?>
+
+<window
+ id="ClearSiteDataDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="clear-site-data-window2"
+ data-l10n-attrs="title, style"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="clear-site-data-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/clearSiteData.ftl"
+ />
+ </linkset>
+ <script src="chrome://browser/content/preferences/dialogs/clearSiteData.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="clear-site-data-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description control="url" data-l10n-id="clear-site-data-description" />
+ <separator class="thin" />
+ <vbox class="options-container">
+ <vbox class="option">
+ <checkbox
+ data-l10n-id="clear-site-data-cookies-empty"
+ id="clearSiteData"
+ checked="true"
+ />
+ <description
+ class="option-description indent"
+ data-l10n-id="clear-site-data-cookies-info"
+ />
+ </vbox>
+ <vbox class="option">
+ <checkbox
+ data-l10n-id="clear-site-data-cache-empty"
+ id="clearCache"
+ checked="true"
+ />
+ <description
+ class="option-description indent"
+ data-l10n-id="clear-site-data-cache-info"
+ />
+ </vbox>
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/colors.js b/browser/components/preferences/dialogs/colors.js
new file mode 100644
index 0000000000..3bb78e5ec1
--- /dev/null
+++ b/browser/components/preferences/dialogs/colors.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+document
+ .getElementById("ColorsDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+Preferences.addAll([
+ { id: "browser.display.document_color_use", type: "int" },
+ { id: "browser.anchor_color", type: "string" },
+ { id: "browser.visited_color", type: "string" },
+ { id: "browser.underline_anchors", type: "bool" },
+ { id: "browser.display.foreground_color", type: "string" },
+ { id: "browser.display.background_color", type: "string" },
+ { id: "browser.display.use_system_colors", type: "bool" },
+]);
diff --git a/browser/components/preferences/dialogs/colors.xhtml b/browser/components/preferences/dialogs/colors.xhtml
new file mode 100644
index 0000000000..8d81273880
--- /dev/null
+++ b/browser/components/preferences/dialogs/colors.xhtml
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="colors-dialog2"
+ data-l10n-attrs="title, style"
+ persist="lastSelected"
+>
+ <dialog
+ id="ColorsDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-fonts-and-colors"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/colors.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="colors-close-key"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <hbox>
+ <groupbox flex="1">
+ <label><html:h2 data-l10n-id="colors-text-and-background" /></label>
+ <hbox align="center">
+ <label
+ data-l10n-id="colors-text-header"
+ control="foregroundtextmenu"
+ />
+ <spacer flex="1" />
+ <html:input
+ type="color"
+ id="foregroundtextmenu"
+ preference="browser.display.foreground_color"
+ />
+ </hbox>
+ <hbox align="center" style="margin-top: 5px">
+ <label data-l10n-id="colors-background" control="backgroundmenu" />
+ <spacer flex="1" />
+ <html:input
+ type="color"
+ id="backgroundmenu"
+ preference="browser.display.background_color"
+ />
+ </hbox>
+ <separator class="thin" />
+ <hbox align="center">
+ <checkbox
+ id="browserUseSystemColors"
+ data-l10n-id="colors-use-system"
+ preference="browser.display.use_system_colors"
+ />
+ </hbox>
+ </groupbox>
+
+ <groupbox flex="1">
+ <label><html:h2 data-l10n-id="colors-links-header" /></label>
+ <hbox align="center">
+ <label
+ data-l10n-id="colors-unvisited-links"
+ control="unvisitedlinkmenu"
+ />
+ <spacer flex="1" />
+ <html:input
+ type="color"
+ id="unvisitedlinkmenu"
+ preference="browser.anchor_color"
+ />
+ </hbox>
+ <hbox align="center" style="margin-top: 5px">
+ <label
+ data-l10n-id="colors-visited-links"
+ control="visitedlinkmenu"
+ />
+ <spacer flex="1" />
+ <html:input
+ type="color"
+ id="visitedlinkmenu"
+ preference="browser.visited_color"
+ />
+ </hbox>
+ <separator class="thin" />
+ <hbox align="center">
+ <checkbox
+ id="browserUnderlineAnchors"
+ data-l10n-id="colors-underline-links"
+ preference="browser.underline_anchors"
+ />
+ </hbox>
+ </groupbox>
+ </hbox>
+
+ <label data-l10n-id="colors-page-override" control="useDocumentColors" />
+ <hbox>
+ <menulist
+ id="useDocumentColors"
+ preference="browser.display.document_color_use"
+ flex="1"
+ >
+ <menupopup>
+ <menuitem
+ data-l10n-id="colors-page-override-option-always"
+ value="2"
+ id="documentColorAlways"
+ />
+ <menuitem
+ data-l10n-id="colors-page-override-option-auto"
+ value="0"
+ id="documentColorAutomatic"
+ />
+ <menuitem
+ data-l10n-id="colors-page-override-option-never"
+ value="1"
+ id="documentColorNever"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <!-- Load the script after the elements for layout issues (bug 1501755). -->
+ <script src="chrome://browser/content/preferences/dialogs/colors.js" />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/connection.js b/browser/components/preferences/dialogs/connection.js
new file mode 100644
index 0000000000..ee669b3762
--- /dev/null
+++ b/browser/components/preferences/dialogs/connection.js
@@ -0,0 +1,381 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /browser/base/content/utilityOverlay.js */
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+/* import-globals-from ../extensionControlled.js */
+
+document
+ .getElementById("ConnectionsDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+Preferences.addAll([
+ // Add network.proxy.autoconfig_url before network.proxy.type so they're
+ // both initialized when network.proxy.type initialization triggers a call to
+ // gConnectionsDialog.updateReloadButton().
+ { id: "network.proxy.autoconfig_url", type: "string" },
+ { id: "network.proxy.type", type: "int" },
+ { id: "network.proxy.http", type: "string" },
+ { id: "network.proxy.http_port", type: "int" },
+ { id: "network.proxy.ssl", type: "string" },
+ { id: "network.proxy.ssl_port", type: "int" },
+ { id: "network.proxy.socks", type: "string" },
+ { id: "network.proxy.socks_port", type: "int" },
+ { id: "network.proxy.socks_version", type: "int" },
+ { id: "network.proxy.socks_remote_dns", type: "bool" },
+ { id: "network.proxy.no_proxies_on", type: "string" },
+ { id: "network.proxy.share_proxy_settings", type: "bool" },
+ { id: "signon.autologin.proxy", type: "bool" },
+ { id: "pref.advanced.proxies.disable_button.reload", type: "bool" },
+ { id: "network.proxy.backup.ssl", type: "string" },
+ { id: "network.proxy.backup.ssl_port", type: "int" },
+]);
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ Preferences.get("network.proxy.type").on(
+ "change",
+ gConnectionsDialog.proxyTypeChanged.bind(gConnectionsDialog)
+ );
+ Preferences.get("network.proxy.socks_version").on(
+ "change",
+ gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog)
+ );
+
+ document
+ .getElementById("disableProxyExtension")
+ .addEventListener(
+ "command",
+ makeDisableControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).bind(
+ gConnectionsDialog
+ )
+ );
+ gConnectionsDialog.updateProxySettingsUI();
+ initializeProxyUI(gConnectionsDialog);
+ gConnectionsDialog.registerSyncPrefListeners();
+ document
+ .getElementById("ConnectionsDialog")
+ .addEventListener("beforeaccept", e =>
+ gConnectionsDialog.beforeAccept(e)
+ );
+ },
+ { once: true, capture: true }
+);
+
+var gConnectionsDialog = {
+ beforeAccept(event) {
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ if (proxyTypePref.value == 2) {
+ this.doAutoconfigURLFixup();
+ return;
+ }
+
+ if (proxyTypePref.value != 1) {
+ return;
+ }
+
+ var httpProxyURLPref = Preferences.get("network.proxy.http");
+ var httpProxyPortPref = Preferences.get("network.proxy.http_port");
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+
+ // If the port is 0 and the proxy server is specified, focus on the port and cancel submission.
+ for (let prefName of ["http", "ssl", "socks"]) {
+ let proxyPortPref = Preferences.get(
+ "network.proxy." + prefName + "_port"
+ );
+ let proxyPref = Preferences.get("network.proxy." + prefName);
+ // Only worry about ports which are currently active. If the share option is on, then ignore
+ // all ports except the HTTP and SOCKS port
+ if (
+ proxyPref.value != "" &&
+ proxyPortPref.value == 0 &&
+ (prefName == "http" || prefName == "socks" || !shareProxiesPref.value)
+ ) {
+ document
+ .getElementById("networkProxy" + prefName.toUpperCase() + "_Port")
+ .focus();
+ event.preventDefault();
+ return;
+ }
+ }
+
+ // In the case of a shared proxy preference, backup the current values and update with the HTTP value
+ if (shareProxiesPref.value) {
+ var proxyServerURLPref = Preferences.get("network.proxy.ssl");
+ var proxyPortPref = Preferences.get("network.proxy.ssl_port");
+ var backupServerURLPref = Preferences.get("network.proxy.backup.ssl");
+ var backupPortPref = Preferences.get("network.proxy.backup.ssl_port");
+ backupServerURLPref.value =
+ backupServerURLPref.value || proxyServerURLPref.value;
+ backupPortPref.value = backupPortPref.value || proxyPortPref.value;
+ proxyServerURLPref.value = httpProxyURLPref.value;
+ proxyPortPref.value = httpProxyPortPref.value;
+ }
+
+ this.sanitizeNoProxiesPref();
+ },
+
+ checkForSystemProxy() {
+ if ("@mozilla.org/system-proxy-settings;1" in Cc) {
+ document.getElementById("systemPref").removeAttribute("hidden");
+ }
+ },
+
+ proxyTypeChanged() {
+ var proxyTypePref = Preferences.get("network.proxy.type");
+
+ // Update http
+ var httpProxyURLPref = Preferences.get("network.proxy.http");
+ httpProxyURLPref.updateControlDisabledState(proxyTypePref.value != 1);
+ var httpProxyPortPref = Preferences.get("network.proxy.http_port");
+ httpProxyPortPref.updateControlDisabledState(proxyTypePref.value != 1);
+
+ // Now update the other protocols
+ this.updateProtocolPrefs();
+
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ shareProxiesPref.updateControlDisabledState(proxyTypePref.value != 1);
+ var autologinProxyPref = Preferences.get("signon.autologin.proxy");
+ autologinProxyPref.updateControlDisabledState(proxyTypePref.value == 0);
+ var noProxiesPref = Preferences.get("network.proxy.no_proxies_on");
+ noProxiesPref.updateControlDisabledState(proxyTypePref.value == 0);
+
+ var autoconfigURLPref = Preferences.get("network.proxy.autoconfig_url");
+ autoconfigURLPref.updateControlDisabledState(proxyTypePref.value != 2);
+
+ this.updateReloadButton();
+
+ document.getElementById("networkProxyNoneLocalhost").hidden =
+ Services.prefs.getBoolPref(
+ "network.proxy.allow_hijacking_localhost",
+ false
+ );
+ },
+
+ updateDNSPref() {
+ var socksVersionPref = Preferences.get("network.proxy.socks_version");
+ var socksDNSPref = Preferences.get("network.proxy.socks_remote_dns");
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ var isDefinitelySocks4 =
+ proxyTypePref.value == 1 && socksVersionPref.value == 4;
+ socksDNSPref.updateControlDisabledState(
+ isDefinitelySocks4 || proxyTypePref.value == 0
+ );
+ return undefined;
+ },
+
+ updateReloadButton() {
+ // Disable the "Reload PAC" button if the selected proxy type is not PAC or
+ // if the current value of the PAC input does not match the value stored
+ // in prefs. Likewise, disable the reload button if PAC is not configured
+ // in prefs.
+
+ var typedURL = document.getElementById("networkProxyAutoconfigURL").value;
+ var proxyTypeCur = Preferences.get("network.proxy.type").value;
+
+ var pacURL = Services.prefs.getCharPref("network.proxy.autoconfig_url");
+ var proxyType = Services.prefs.getIntPref("network.proxy.type");
+
+ var disableReloadPref = Preferences.get(
+ "pref.advanced.proxies.disable_button.reload"
+ );
+ disableReloadPref.updateControlDisabledState(
+ proxyTypeCur != 2 || proxyType != 2 || typedURL != pacURL
+ );
+ },
+
+ readProxyType() {
+ this.proxyTypeChanged();
+ return undefined;
+ },
+
+ updateProtocolPrefs() {
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ var proxyPrefs = ["ssl", "socks"];
+ for (var i = 0; i < proxyPrefs.length; ++i) {
+ var proxyServerURLPref = Preferences.get(
+ "network.proxy." + proxyPrefs[i]
+ );
+ var proxyPortPref = Preferences.get(
+ "network.proxy." + proxyPrefs[i] + "_port"
+ );
+
+ // Restore previous per-proxy custom settings, if present.
+ if (proxyPrefs[i] != "socks" && !shareProxiesPref.value) {
+ var backupServerURLPref = Preferences.get(
+ "network.proxy.backup." + proxyPrefs[i]
+ );
+ var backupPortPref = Preferences.get(
+ "network.proxy.backup." + proxyPrefs[i] + "_port"
+ );
+ if (backupServerURLPref.hasUserValue) {
+ proxyServerURLPref.value = backupServerURLPref.value;
+ backupServerURLPref.reset();
+ }
+ if (backupPortPref.hasUserValue) {
+ proxyPortPref.value = backupPortPref.value;
+ backupPortPref.reset();
+ }
+ }
+
+ proxyServerURLPref.updateElements();
+ proxyPortPref.updateElements();
+ let prefIsShared = proxyPrefs[i] != "socks" && shareProxiesPref.value;
+ proxyServerURLPref.updateControlDisabledState(
+ proxyTypePref.value != 1 || prefIsShared
+ );
+ proxyPortPref.updateControlDisabledState(
+ proxyTypePref.value != 1 || prefIsShared
+ );
+ }
+ var socksVersionPref = Preferences.get("network.proxy.socks_version");
+ socksVersionPref.updateControlDisabledState(proxyTypePref.value != 1);
+ this.updateDNSPref();
+ return undefined;
+ },
+
+ readProxyProtocolPref(aProtocol, aIsPort) {
+ if (aProtocol != "socks") {
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ if (shareProxiesPref.value) {
+ var pref = Preferences.get(
+ "network.proxy.http" + (aIsPort ? "_port" : "")
+ );
+ return pref.value;
+ }
+
+ var backupPref = Preferences.get(
+ "network.proxy.backup." + aProtocol + (aIsPort ? "_port" : "")
+ );
+ return backupPref.hasUserValue ? backupPref.value : undefined;
+ }
+ return undefined;
+ },
+
+ reloadPAC() {
+ Cc["@mozilla.org/network/protocol-proxy-service;1"]
+ .getService()
+ .reloadPAC();
+ },
+
+ doAutoconfigURLFixup() {
+ var autoURL = document.getElementById("networkProxyAutoconfigURL");
+ var autoURLPref = Preferences.get("network.proxy.autoconfig_url");
+ try {
+ autoURLPref.value = autoURL.value = Services.uriFixup.getFixupURIInfo(
+ autoURL.value
+ ).preferredURI.spec;
+ } catch (ex) {}
+ },
+
+ sanitizeNoProxiesPref() {
+ var noProxiesPref = Preferences.get("network.proxy.no_proxies_on");
+ // replace substrings of ; and \n with commas if they're neither immediately
+ // preceded nor followed by a valid separator character
+ noProxiesPref.value = noProxiesPref.value.replace(
+ /([^, \n;])[;\n]+(?![,\n;])/g,
+ "$1,"
+ );
+ // replace any remaining ; and \n since some may follow commas, etc.
+ noProxiesPref.value = noProxiesPref.value.replace(/[;\n]/g, "");
+ },
+
+ readHTTPProxyServer() {
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ if (shareProxiesPref.value) {
+ this.updateProtocolPrefs();
+ }
+ return undefined;
+ },
+
+ readHTTPProxyPort() {
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ if (shareProxiesPref.value) {
+ this.updateProtocolPrefs();
+ }
+ return undefined;
+ },
+
+ getProxyControls() {
+ let controlGroup = document.getElementById("networkProxyType");
+ return [
+ ...controlGroup.querySelectorAll(":scope > radio"),
+ ...controlGroup.querySelectorAll("label"),
+ ...controlGroup.querySelectorAll("input"),
+ ...controlGroup.querySelectorAll("checkbox"),
+ ...document.querySelectorAll("#networkProxySOCKSVersion > radio"),
+ ...document.querySelectorAll("#ConnectionsDialogPane > checkbox"),
+ ];
+ },
+
+ // Update the UI to show/hide the extension controlled message for
+ // proxy settings.
+ async updateProxySettingsUI() {
+ let isLocked = API_PROXY_PREFS.some(pref =>
+ Services.prefs.prefIsLocked(pref)
+ );
+
+ function setInputsDisabledState(isControlled) {
+ for (let element of gConnectionsDialog.getProxyControls()) {
+ element.disabled = isControlled;
+ }
+ gConnectionsDialog.proxyTypeChanged();
+ }
+
+ if (isLocked) {
+ // An extension can't control this setting if any pref is locked.
+ hideControllingExtension(PROXY_KEY);
+ } else {
+ handleControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).then(
+ setInputsDisabledState
+ );
+ }
+ },
+
+ registerSyncPrefListeners() {
+ function setSyncFromPrefListener(element_id, callback) {
+ Preferences.addSyncFromPrefListener(
+ document.getElementById(element_id),
+ callback
+ );
+ }
+ setSyncFromPrefListener("networkProxyType", () => this.readProxyType());
+ setSyncFromPrefListener("networkProxyHTTP", () =>
+ this.readHTTPProxyServer()
+ );
+ setSyncFromPrefListener("networkProxyHTTP_Port", () =>
+ this.readHTTPProxyPort()
+ );
+ setSyncFromPrefListener("shareAllProxies", () =>
+ this.updateProtocolPrefs()
+ );
+ setSyncFromPrefListener("networkProxySSL", () =>
+ this.readProxyProtocolPref("ssl", false)
+ );
+ setSyncFromPrefListener("networkProxySSL_Port", () =>
+ this.readProxyProtocolPref("ssl", true)
+ );
+ setSyncFromPrefListener("networkProxySOCKS", () =>
+ this.readProxyProtocolPref("socks", false)
+ );
+ setSyncFromPrefListener("networkProxySOCKS_Port", () =>
+ this.readProxyProtocolPref("socks", true)
+ );
+ },
+};
diff --git a/browser/components/preferences/dialogs/connection.xhtml b/browser/components/preferences/dialogs/connection.xhtml
new file mode 100644
index 0000000000..4f161a710d
--- /dev/null
+++ b/browser/components/preferences/dialogs/connection.xhtml
@@ -0,0 +1,244 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="connection-window2"
+ data-l10n-attrs="title, style"
+ persist="lastSelected"
+ onload="gConnectionsDialog.checkForSystemProxy();"
+>
+ <dialog
+ id="ConnectionsDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-connection-settings"
+ >
+ <!-- Used for extension-controlled lockdown message -->
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/connection.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/preferences.ftl"
+ />
+ <html:link rel="localization" href="branding/brand.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/extensionControlled.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="connection-close-key"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <script src="chrome://browser/content/preferences/dialogs/connection.js" />
+
+ <hbox
+ id="proxyExtensionContent"
+ align="start"
+ hidden="true"
+ class="extension-controlled"
+ >
+ <description control="disableProxyExtension" flex="1" />
+ <button
+ id="disableProxyExtension"
+ class="extension-controlled-button accessory-button"
+ data-l10n-id="connection-disable-extension"
+ />
+ </hbox>
+
+ <groupbox>
+ <label><html:h2 data-l10n-id="connection-proxy-configure" /></label>
+
+ <radiogroup id="networkProxyType" preference="network.proxy.type">
+ <radio value="0" data-l10n-id="connection-proxy-option-no" />
+ <radio value="4" data-l10n-id="connection-proxy-option-auto" />
+ <radio
+ value="5"
+ data-l10n-id="connection-proxy-option-system"
+ id="systemPref"
+ hidden="true"
+ />
+ <radio value="1" data-l10n-id="connection-proxy-option-manual" />
+ <box id="proxy-grid" class="indent" flex="1">
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="connection-proxy-http"
+ control="networkProxyHTTP"
+ />
+ </hbox>
+ <hbox align="center">
+ <html:input
+ id="networkProxyHTTP"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.http"
+ />
+ <label
+ data-l10n-id="connection-proxy-http-port"
+ control="networkProxyHTTP_Port"
+ />
+ <html:input
+ id="networkProxyHTTP_Port"
+ class="proxy-port-input"
+ hidespinbuttons="true"
+ type="number"
+ min="0"
+ max="65535"
+ preference="network.proxy.http_port"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <hbox />
+ <hbox>
+ <checkbox
+ id="shareAllProxies"
+ data-l10n-id="connection-proxy-https-sharing"
+ preference="network.proxy.share_proxy_settings"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="connection-proxy-https"
+ control="networkProxySSL"
+ />
+ </hbox>
+ <hbox align="center">
+ <html:input
+ id="networkProxySSL"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.ssl"
+ />
+ <label
+ data-l10n-id="connection-proxy-ssl-port"
+ control="networkProxySSL_Port"
+ />
+ <html:input
+ id="networkProxySSL_Port"
+ class="proxy-port-input"
+ hidespinbuttons="true"
+ type="number"
+ min="0"
+ max="65535"
+ size="5"
+ preference="network.proxy.ssl_port"
+ />
+ </hbox>
+ </html:div>
+ <separator class="thin" />
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="connection-proxy-socks"
+ control="networkProxySOCKS"
+ />
+ </hbox>
+ <hbox align="center">
+ <html:input
+ id="networkProxySOCKS"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.socks"
+ />
+ <label
+ data-l10n-id="connection-proxy-socks-port"
+ control="networkProxySOCKS_Port"
+ />
+ <html:input
+ id="networkProxySOCKS_Port"
+ class="proxy-port-input"
+ hidespinbuttons="true"
+ type="number"
+ min="0"
+ max="65535"
+ size="5"
+ preference="network.proxy.socks_port"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <spacer />
+ <box pack="start">
+ <radiogroup
+ id="networkProxySOCKSVersion"
+ orient="horizontal"
+ preference="network.proxy.socks_version"
+ >
+ <radio
+ id="networkProxySOCKSVersion4"
+ value="4"
+ data-l10n-id="connection-proxy-socks4"
+ />
+ <radio
+ id="networkProxySOCKSVersion5"
+ value="5"
+ data-l10n-id="connection-proxy-socks5"
+ />
+ </radiogroup>
+ </box>
+ </html:div>
+ </box>
+ <radio value="2" data-l10n-id="connection-proxy-autotype" />
+ <hbox class="indent" flex="1" align="center">
+ <html:input
+ id="networkProxyAutoconfigURL"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.autoconfig_url"
+ oninput="gConnectionsDialog.updateReloadButton();"
+ />
+ <button
+ id="autoReload"
+ data-l10n-id="connection-proxy-reload"
+ oncommand="gConnectionsDialog.reloadPAC();"
+ preference="pref.advanced.proxies.disable_button.reload"
+ />
+ </hbox>
+ </radiogroup>
+ </groupbox>
+ <separator class="thin" />
+ <label data-l10n-id="connection-proxy-noproxy" control="networkProxyNone" />
+ <html:textarea
+ id="networkProxyNone"
+ preference="network.proxy.no_proxies_on"
+ rows="2"
+ />
+ <label
+ control="networkProxyNone"
+ data-l10n-id="connection-proxy-noproxy-desc"
+ />
+ <label
+ id="networkProxyNoneLocalhost"
+ control="networkProxyNone"
+ data-l10n-id="connection-proxy-noproxy-localhost-desc-2"
+ />
+ <separator class="thin" />
+ <checkbox
+ id="autologinProxy"
+ data-l10n-id="connection-proxy-autologin"
+ preference="signon.autologin.proxy"
+ />
+ <checkbox
+ id="networkProxySOCKSRemoteDNS"
+ preference="network.proxy.socks_remote_dns"
+ data-l10n-id="connection-proxy-socks-remote-dns"
+ />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/containers.js b/browser/components/preferences/dialogs/containers.js
new file mode 100644
index 0000000000..14526545b6
--- /dev/null
+++ b/browser/components/preferences/dialogs/containers.js
@@ -0,0 +1,167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { ContextualIdentityService } = ChromeUtils.importESModule(
+ "resource://gre/modules/ContextualIdentityService.sys.mjs"
+);
+
+/**
+ * We want to set the window title immediately to prevent flickers.
+ */
+function setTitle() {
+ let params = window.arguments[0] || {};
+
+ let winElem = document.documentElement;
+ if (params.userContextId) {
+ document.l10n.setAttributes(winElem, "containers-window-update-settings2", {
+ name: params.identity.name,
+ });
+ } else {
+ document.l10n.setAttributes(winElem, "containers-window-new2");
+ }
+}
+setTitle();
+
+let gContainersManager = {
+ icons: [
+ "fingerprint",
+ "briefcase",
+ "dollar",
+ "cart",
+ "vacation",
+ "gift",
+ "food",
+ "fruit",
+ "pet",
+ "tree",
+ "chill",
+ "circle",
+ "fence",
+ ],
+
+ colors: [
+ "blue",
+ "turquoise",
+ "green",
+ "yellow",
+ "orange",
+ "red",
+ "pink",
+ "purple",
+ "toolbar",
+ ],
+
+ onLoad() {
+ let params = window.arguments[0] || {};
+ this.init(params);
+ },
+
+ init(aParams) {
+ this._dialog = document.querySelector("dialog");
+ this.userContextId = aParams.userContextId || null;
+ this.identity = aParams.identity;
+
+ const iconWrapper = document.getElementById("iconWrapper");
+ iconWrapper.appendChild(this.createIconButtons());
+
+ const colorWrapper = document.getElementById("colorWrapper");
+ colorWrapper.appendChild(this.createColorSwatches());
+
+ if (this.identity.name) {
+ const name = document.getElementById("name");
+ name.value = this.identity.name;
+ this.checkForm();
+ }
+
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ // This is to prevent layout jank caused by the svgs and outlines rendering at different times
+ document.getElementById("containers-content").removeAttribute("hidden");
+ },
+
+ uninit() {},
+
+ // Check if name is provided to determine if the form can be submitted
+ checkForm() {
+ const name = document.getElementById("name");
+ this._dialog.setAttribute("buttondisabledaccept", !name.value.trim());
+ },
+
+ createIconButtons(defaultIcon) {
+ let radiogroup = document.createXULElement("radiogroup");
+ radiogroup.setAttribute("id", "icon");
+ radiogroup.className = "icon-buttons radio-buttons";
+
+ for (let icon of this.icons) {
+ let iconSwatch = document.createXULElement("radio");
+ iconSwatch.id = "iconbutton-" + icon;
+ iconSwatch.name = "icon";
+ iconSwatch.type = "radio";
+ iconSwatch.value = icon;
+
+ if (this.identity.icon && this.identity.icon == icon) {
+ iconSwatch.setAttribute("selected", true);
+ }
+
+ document.l10n.setAttributes(iconSwatch, `containers-icon-${icon}`);
+ let iconElement = document.createXULElement("hbox");
+ iconElement.className = "userContext-icon";
+ iconElement.classList.add("identity-icon-" + icon);
+
+ iconSwatch.appendChild(iconElement);
+ radiogroup.appendChild(iconSwatch);
+ }
+
+ return radiogroup;
+ },
+
+ createColorSwatches(defaultColor) {
+ let radiogroup = document.createXULElement("radiogroup");
+ radiogroup.setAttribute("id", "color");
+ radiogroup.className = "radio-buttons";
+
+ for (let color of this.colors) {
+ let colorSwatch = document.createXULElement("radio");
+ colorSwatch.id = "colorswatch-" + color;
+ colorSwatch.name = "color";
+ colorSwatch.type = "radio";
+ colorSwatch.value = color;
+
+ if (this.identity.color && this.identity.color == color) {
+ colorSwatch.setAttribute("selected", true);
+ }
+
+ document.l10n.setAttributes(colorSwatch, `containers-color-${color}`);
+ let iconElement = document.createXULElement("hbox");
+ iconElement.className = "userContext-icon";
+ iconElement.classList.add("identity-icon-circle");
+ iconElement.classList.add("identity-color-" + color);
+
+ colorSwatch.appendChild(iconElement);
+ radiogroup.appendChild(colorSwatch);
+ }
+ return radiogroup;
+ },
+
+ onApplyChanges() {
+ let icon = document.getElementById("icon").value;
+ let color = document.getElementById("color").value;
+ let name = document.getElementById("name").value;
+
+ if (!this.icons.includes(icon)) {
+ throw new Error("Internal error. The icon value doesn't match.");
+ }
+
+ if (!this.colors.includes(color)) {
+ throw new Error("Internal error. The color value doesn't match.");
+ }
+
+ if (this.userContextId) {
+ ContextualIdentityService.update(this.userContextId, name, icon, color);
+ } else {
+ ContextualIdentityService.create(name, icon, color);
+ }
+ window.parent.location.reload();
+ },
+};
diff --git a/browser/components/preferences/dialogs/containers.xhtml b/browser/components/preferences/dialogs/containers.xhtml
new file mode 100644
index 0000000000..446a78aee0
--- /dev/null
+++ b/browser/components/preferences/dialogs/containers.xhtml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/containers-dialog.css" type="text/css"?>
+
+<window
+ id="ContainersDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-attrs="title, style"
+ onload="gContainersManager.onLoad();"
+ onunload="gContainersManager.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept"
+ buttondisabledaccept="true"
+ data-l10n-id="containers-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/containers.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/containers.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="containers-window-close"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane" hidden="true" id="containers-content">
+ <hbox align="start">
+ <label
+ id="nameLabel"
+ control="name"
+ data-l10n-id="containers-name-label"
+ data-l10n-attrs="style"
+ />
+ <html:input
+ id="name"
+ type="text"
+ data-l10n-id="containers-name-text"
+ oninput="gContainersManager.checkForm();"
+ />
+ </hbox>
+ <hbox align="center" id="colorWrapper">
+ <label
+ id="colorLabel"
+ control="color"
+ data-l10n-id="containers-color-label"
+ data-l10n-attrs="style"
+ />
+ </hbox>
+ <hbox align="center" id="iconWrapper">
+ <label
+ id="iconLabel"
+ control="icon"
+ data-l10n-id="containers-icon-label"
+ data-l10n-attrs="style"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/dohExceptions.js b/browser/components/preferences/dialogs/dohExceptions.js
new file mode 100644
index 0000000000..f4c1d4d80d
--- /dev/null
+++ b/browser/components/preferences/dialogs/dohExceptions.js
@@ -0,0 +1,287 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var gDoHExceptionsManager = {
+ _exceptions: new Set(),
+ _list: null,
+ _prefLocked: false,
+
+ init() {
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ this._btnAddException = document.getElementById("btnAddException");
+ this._removeButton = document.getElementById("removeException");
+ this._removeAllButton = document.getElementById("removeAllExceptions");
+ this._list = document.getElementById("permissionsBox");
+
+ this._urlField = document.getElementById("url");
+ this.onExceptionInput();
+
+ this._loadExceptions();
+ this.buildExceptionList();
+
+ this._urlField.focus();
+
+ this._prefLocked = Services.prefs.prefIsLocked(
+ "network.trr.excluded-domains"
+ );
+
+ this._btnAddException.disabled = this._prefLocked;
+ document.getElementById("exceptionDialog").getButton("accept").disabled =
+ this._prefLocked;
+ this._urlField.disabled = this._prefLocked;
+ },
+
+ _loadExceptions() {
+ let exceptionsFromPref = Services.prefs.getStringPref(
+ "network.trr.excluded-domains"
+ );
+
+ if (!exceptionsFromPref?.trim()) {
+ return;
+ }
+
+ let exceptions = exceptionsFromPref.trim().split(",");
+ for (let exception of exceptions) {
+ let trimmed = exception.trim();
+ if (trimmed) {
+ this._exceptions.add(trimmed);
+ }
+ }
+ },
+
+ addException() {
+ if (this._prefLocked) {
+ return;
+ }
+
+ let textbox = document.getElementById("url");
+ let inputValue = textbox.value.trim(); // trim any leading and trailing space
+ if (!inputValue.startsWith("http:") && !inputValue.startsWith("https:")) {
+ inputValue = `http://${inputValue}`;
+ }
+ let domain = "";
+ try {
+ let uri = Services.io.newURI(inputValue);
+ domain = uri.host;
+ } catch (ex) {
+ document.l10n
+ .formatValues([
+ { id: "permissions-invalid-uri-title" },
+ { id: "permissions-invalid-uri-label" },
+ ])
+ .then(([title, message]) => {
+ Services.prompt.alert(window, title, message);
+ });
+ return;
+ }
+
+ if (!this._exceptions.has(domain)) {
+ this._exceptions.add(domain);
+ this.buildExceptionList();
+ }
+
+ textbox.value = "";
+ textbox.focus();
+
+ // covers a case where the site exists already, so the buttons don't disable
+ this.onExceptionInput();
+
+ // enable "remove all" button as needed
+ this._setRemoveButtonState();
+ },
+
+ onExceptionInput() {
+ this._btnAddException.disabled = !this._urlField.value;
+ },
+
+ onExceptionKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
+ this._btnAddException.click();
+ if (document.activeElement == this._urlField) {
+ event.preventDefault();
+ }
+ }
+ },
+
+ onListBoxKeyPress(event) {
+ if (!this._list.selectedItem) {
+ return;
+ }
+
+ if (this._prefLocked) {
+ return;
+ }
+
+ if (
+ event.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ this.onExceptionDelete();
+ event.preventDefault();
+ }
+ },
+
+ onListBoxSelect() {
+ this._setRemoveButtonState();
+ },
+
+ _removeExceptionFromList(exception) {
+ this._exceptions.delete(exception);
+ let exceptionlistitem = document.getElementsByAttribute(
+ "domain",
+ exception
+ )[0];
+ if (exceptionlistitem) {
+ exceptionlistitem.remove();
+ }
+ },
+
+ onExceptionDelete() {
+ let richlistitem = this._list.selectedItem;
+ let exception = richlistitem.getAttribute("domain");
+
+ this._removeExceptionFromList(exception);
+
+ this._setRemoveButtonState();
+ },
+
+ onAllExceptionsDelete() {
+ for (let exception of this._exceptions.values()) {
+ this._removeExceptionFromList(exception);
+ }
+
+ this._setRemoveButtonState();
+ },
+
+ _createExceptionListItem(exception) {
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("domain", exception);
+ let row = document.createXULElement("hbox");
+ row.setAttribute("style", "flex: 1");
+
+ let hbox = document.createXULElement("hbox");
+ let website = document.createXULElement("label");
+ website.setAttribute("class", "website-name-value");
+ website.setAttribute("value", exception);
+ hbox.setAttribute("class", "website-name");
+ hbox.setAttribute("style", "flex: 3 3; width: 0");
+ hbox.appendChild(website);
+ row.appendChild(hbox);
+
+ richlistitem.appendChild(row);
+ return richlistitem;
+ },
+
+ _sortExceptions(list, frag, column) {
+ let sortDirection;
+
+ if (!column) {
+ column = document.querySelector("treecol[data-isCurrentSortCol=true]");
+ sortDirection =
+ column.getAttribute("data-last-sortDirection") || "ascending";
+ } else {
+ sortDirection = column.getAttribute("data-last-sortDirection");
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = (a, b) => {
+ return comp.compare(a.getAttribute("domain"), b.getAttribute("domain"));
+ };
+
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.querySelectorAll("richlistitem"));
+
+ if (sortDirection === "descending") {
+ items.sort((a, b) => sortFunc(b, a));
+ } else {
+ items.sort(sortFunc);
+ }
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ let cols = list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("data-isCurrentSortCol");
+ c.removeAttribute("sortDirection");
+ });
+ column.setAttribute("data-isCurrentSortCol", "true");
+ column.setAttribute("sortDirection", sortDirection);
+ column.setAttribute("data-last-sortDirection", sortDirection);
+ },
+
+ _setRemoveButtonState() {
+ if (!this._list) {
+ return;
+ }
+
+ if (this._prefLocked) {
+ this._removeAllButton.disabled = true;
+ this._removeButton.disabled = true;
+ return;
+ }
+
+ let hasSelection = this._list.selectedIndex >= 0;
+
+ this._removeButton.disabled = !hasSelection;
+ let disabledItems = this._list.querySelectorAll(
+ "label.website-name-value[disabled='true']"
+ );
+
+ this._removeAllButton.disabled =
+ this._list.itemCount == disabledItems.length;
+ },
+
+ onApplyChanges() {
+ if (this._exceptions.size == 0) {
+ Services.prefs.setStringPref("network.trr.excluded-domains", "");
+ return;
+ }
+
+ let exceptions = Array.from(this._exceptions);
+ let exceptionPrefString = exceptions.join(",");
+
+ Services.prefs.setStringPref(
+ "network.trr.excluded-domains",
+ exceptionPrefString
+ );
+ },
+
+ buildExceptionList(sortCol) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+ let frag = document.createDocumentFragment();
+
+ let exceptions = Array.from(this._exceptions.values());
+
+ for (let exception of exceptions) {
+ let richlistitem = this._createExceptionListItem(exception);
+ frag.appendChild(richlistitem);
+ }
+
+ // Sort exceptions.
+ this._sortExceptions(this._list, frag, sortCol);
+
+ this._list.appendChild(frag);
+
+ this._setRemoveButtonState();
+ },
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ gDoHExceptionsManager.init();
+});
diff --git a/browser/components/preferences/dialogs/dohExceptions.xhtml b/browser/components/preferences/dialogs/dohExceptions.xhtml
new file mode 100644
index 0000000000..617e9fbfb0
--- /dev/null
+++ b/browser/components/preferences/dialogs/dohExceptions.xhtml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="DoHExceptionsDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="permissions-exceptions-doh-window"
+ data-l10n-attrs="title, style"
+ persist="width height"
+>
+ <dialog
+ id="exceptionDialog"
+ buttons="accept,cancel"
+ data-l10n-id="permission-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/permissions.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/dohExceptions.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="permissions-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description
+ id="dohExceptionText"
+ control="url"
+ data-l10n-id="permissions-exceptions-manage-doh-desc"
+ />
+ <separator class="thin" />
+ <label
+ id="urlLabel"
+ control="url"
+ data-l10n-id="permissions-doh-entry-field"
+ />
+ <hbox align="start">
+ <html:input
+ id="url"
+ type="text"
+ style="flex: 1"
+ oninput="gDoHExceptionsManager.onExceptionInput();"
+ onkeypress="gDoHExceptionsManager.onExceptionKeyPress(event);"
+ />
+ </hbox>
+ <hbox pack="end">
+ <button
+ id="btnAddException"
+ disabled="true"
+ data-l10n-id="permissions-doh-add-exception"
+ oncommand="gDoHExceptionsManager.addException();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <listheader>
+ <treecol
+ id="siteCol"
+ data-l10n-id="permissions-doh-col"
+ style="flex: 3 3 auto; width: 0"
+ data-isCurrentSortCol="true"
+ onclick="gDoHExceptionsManager.buildExceptionList(event.target)"
+ />
+ </listheader>
+ <richlistbox
+ id="permissionsBox"
+ selected="false"
+ onkeypress="gDoHExceptionsManager.onListBoxKeyPress(event);"
+ onselect="gDoHExceptionsManager.onListBoxSelect();"
+ />
+ </vbox>
+
+ <hbox class="actionButtons">
+ <button
+ id="removeException"
+ disabled="true"
+ data-l10n-id="permissions-doh-remove"
+ oncommand="gDoHExceptionsManager.onExceptionDelete();"
+ />
+ <button
+ id="removeAllExceptions"
+ data-l10n-id="permissions-doh-remove-all"
+ oncommand="gDoHExceptionsManager.onAllExceptionsDelete();"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/fonts.js b/browser/components/preferences/dialogs/fonts.js
new file mode 100644
index 0000000000..ccc2f8faca
--- /dev/null
+++ b/browser/components/preferences/dialogs/fonts.js
@@ -0,0 +1,173 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /browser/base/content/utilityOverlay.js */
+/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */
+
+// browser.display.languageList LOCK ALL when LOCKED
+
+const kDefaultFontType = "font.default.%LANG%";
+const kFontNameFmtSerif = "font.name.serif.%LANG%";
+const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%";
+const kFontNameFmtMonospace = "font.name.monospace.%LANG%";
+const kFontNameListFmtSerif = "font.name-list.serif.%LANG%";
+const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
+const kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%";
+const kFontSizeFmtVariable = "font.size.variable.%LANG%";
+const kFontSizeFmtFixed = "font.size.monospace.%LANG%";
+const kFontMinSizeFmt = "font.minimum-size.%LANG%";
+
+document
+ .getElementById("FontsDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+window.addEventListener("load", () => gFontsDialog.onLoad());
+
+Preferences.addAll([
+ { id: "font.language.group", type: "wstring" },
+ { id: "browser.display.use_document_fonts", type: "int" },
+]);
+
+var gFontsDialog = {
+ _selectLanguageGroupPromise: Promise.resolve(),
+
+ onLoad() {
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("selectLangs"),
+ () => this.readFontLanguageGroup()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("useDocumentFonts"),
+ () => this.readUseDocumentFonts()
+ );
+ Preferences.addSyncToPrefListener(
+ document.getElementById("useDocumentFonts"),
+ () => this.writeUseDocumentFonts()
+ );
+ for (let id of ["serif", "sans-serif", "monospace"]) {
+ let el = document.getElementById(id);
+ Preferences.addSyncFromPrefListener(el, () =>
+ FontBuilder.readFontSelection(el)
+ );
+ }
+ },
+
+ _selectLanguageGroup(aLanguageGroup) {
+ this._selectLanguageGroupPromise = (async () => {
+ // Avoid overlapping language group selections by awaiting the resolution
+ // of the previous one. We do this because this function is re-entrant,
+ // as inserting <preference> elements into the DOM sometimes triggers a call
+ // back into this function. And since this function is also asynchronous,
+ // that call can enter this function before the previous run has completed,
+ // which would corrupt the font menulists. Awaiting the previous call's
+ // resolution avoids that fate.
+ await this._selectLanguageGroupPromise;
+
+ var prefs = [
+ {
+ format: kDefaultFontType,
+ type: "string",
+ element: "defaultFontType",
+ fonttype: null,
+ },
+ {
+ format: kFontNameFmtSerif,
+ type: "fontname",
+ element: "serif",
+ fonttype: "serif",
+ },
+ {
+ format: kFontNameFmtSansSerif,
+ type: "fontname",
+ element: "sans-serif",
+ fonttype: "sans-serif",
+ },
+ {
+ format: kFontNameFmtMonospace,
+ type: "fontname",
+ element: "monospace",
+ fonttype: "monospace",
+ },
+ {
+ format: kFontNameListFmtSerif,
+ type: "unichar",
+ element: null,
+ fonttype: "serif",
+ },
+ {
+ format: kFontNameListFmtSansSerif,
+ type: "unichar",
+ element: null,
+ fonttype: "sans-serif",
+ },
+ {
+ format: kFontNameListFmtMonospace,
+ type: "unichar",
+ element: null,
+ fonttype: "monospace",
+ },
+ {
+ format: kFontSizeFmtVariable,
+ type: "int",
+ element: "sizeVar",
+ fonttype: null,
+ },
+ {
+ format: kFontSizeFmtFixed,
+ type: "int",
+ element: "sizeMono",
+ fonttype: null,
+ },
+ {
+ format: kFontMinSizeFmt,
+ type: "int",
+ element: "minSize",
+ fonttype: null,
+ },
+ ];
+ for (var i = 0; i < prefs.length; ++i) {
+ var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup);
+ var preference = Preferences.get(name);
+ if (!preference) {
+ preference = Preferences.add({ id: name, type: prefs[i].type });
+ }
+
+ if (!prefs[i].element) {
+ continue;
+ }
+
+ var element = document.getElementById(prefs[i].element);
+ if (element) {
+ element.setAttribute("preference", preference.id);
+
+ if (prefs[i].fonttype) {
+ await FontBuilder.buildFontList(
+ aLanguageGroup,
+ prefs[i].fonttype,
+ element
+ );
+ }
+
+ preference.setElementValue(element);
+ }
+ }
+ })().catch(console.error);
+ },
+
+ readFontLanguageGroup() {
+ var languagePref = Preferences.get("font.language.group");
+ this._selectLanguageGroup(languagePref.value);
+ return undefined;
+ },
+
+ readUseDocumentFonts() {
+ var preference = Preferences.get("browser.display.use_document_fonts");
+ return preference.value == 1;
+ },
+
+ writeUseDocumentFonts() {
+ var useDocumentFonts = document.getElementById("useDocumentFonts");
+ return useDocumentFonts.checked ? 1 : 0;
+ },
+};
diff --git a/browser/components/preferences/dialogs/fonts.xhtml b/browser/components/preferences/dialogs/fonts.xhtml
new file mode 100644
index 0000000000..b5c9266384
--- /dev/null
+++ b/browser/components/preferences/dialogs/fonts.xhtml
@@ -0,0 +1,251 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="fonts-window"
+ data-l10n-attrs="title"
+ persist="lastSelected"
+>
+ <dialog
+ id="FontsDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-fonts-and-colors"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/fonts.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="fonts-window-close"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <!-- Fonts for: [ Language ] -->
+ <groupbox>
+ <hbox align="center">
+ <label control="selectLangs"
+ ><html:h2 data-l10n-id="fonts-langgroup-header"
+ /></label>
+ </hbox>
+ <menulist id="selectLangs" preference="font.language.group">
+ <menupopup>
+ <menuitem value="ar" data-l10n-id="fonts-langgroup-arabic" />
+ <menuitem value="x-armn" data-l10n-id="fonts-langgroup-armenian" />
+ <menuitem value="x-beng" data-l10n-id="fonts-langgroup-bengali" />
+ <menuitem
+ value="zh-CN"
+ data-l10n-id="fonts-langgroup-simpl-chinese"
+ />
+ <menuitem
+ value="zh-HK"
+ data-l10n-id="fonts-langgroup-trad-chinese-hk"
+ />
+ <menuitem value="zh-TW" data-l10n-id="fonts-langgroup-trad-chinese" />
+ <menuitem
+ value="x-cyrillic"
+ data-l10n-id="fonts-langgroup-cyrillic"
+ />
+ <menuitem
+ value="x-devanagari"
+ data-l10n-id="fonts-langgroup-devanagari"
+ />
+ <menuitem value="x-ethi" data-l10n-id="fonts-langgroup-ethiopic" />
+ <menuitem value="x-geor" data-l10n-id="fonts-langgroup-georgian" />
+ <menuitem value="el" data-l10n-id="fonts-langgroup-el" />
+ <menuitem value="x-gujr" data-l10n-id="fonts-langgroup-gujarati" />
+ <menuitem value="x-guru" data-l10n-id="fonts-langgroup-gurmukhi" />
+ <menuitem value="he" data-l10n-id="fonts-langgroup-hebrew" />
+ <menuitem value="ja" data-l10n-id="fonts-langgroup-japanese" />
+ <menuitem value="x-knda" data-l10n-id="fonts-langgroup-kannada" />
+ <menuitem value="x-khmr" data-l10n-id="fonts-langgroup-khmer" />
+ <menuitem value="ko" data-l10n-id="fonts-langgroup-korean" />
+ <menuitem value="x-western" data-l10n-id="fonts-langgroup-latin" />
+ <menuitem value="x-mlym" data-l10n-id="fonts-langgroup-malayalam" />
+ <menuitem value="x-math" data-l10n-id="fonts-langgroup-math" />
+ <menuitem value="x-orya" data-l10n-id="fonts-langgroup-odia" />
+ <menuitem value="x-sinh" data-l10n-id="fonts-langgroup-sinhala" />
+ <menuitem value="x-tamil" data-l10n-id="fonts-langgroup-tamil" />
+ <menuitem value="x-telu" data-l10n-id="fonts-langgroup-telugu" />
+ <menuitem value="th" data-l10n-id="fonts-langgroup-thai" />
+ <menuitem value="x-tibt" data-l10n-id="fonts-langgroup-tibetan" />
+ <menuitem value="x-cans" data-l10n-id="fonts-langgroup-canadian" />
+ <menuitem value="x-unicode" data-l10n-id="fonts-langgroup-other" />
+ </menupopup>
+ </menulist>
+
+ <separator class="thin" />
+
+ <box id="font-chooser-group">
+ <!-- proportional row -->
+ <hbox align="center" pack="end">
+ <label
+ data-l10n-id="fonts-proportional-header"
+ control="defaultFontType"
+ />
+ </hbox>
+ <menulist id="defaultFontType">
+ <menupopup>
+ <menuitem value="serif" data-l10n-id="fonts-default-serif" />
+ <menuitem
+ value="sans-serif"
+ data-l10n-id="fonts-default-sans-serif"
+ />
+ </menupopup>
+ </menulist>
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-proportional-size" control="sizeVar" />
+ </hbox>
+ <menulist id="sizeVar" delayprefsave="true">
+ <menupopup>
+ <menuitem value="9" label="9" />
+ <menuitem value="10" label="10" />
+ <menuitem value="11" label="11" />
+ <menuitem value="12" label="12" />
+ <menuitem value="13" label="13" />
+ <menuitem value="14" label="14" />
+ <menuitem value="15" label="15" />
+ <menuitem value="16" label="16" />
+ <menuitem value="17" label="17" />
+ <menuitem value="18" label="18" />
+ <menuitem value="20" label="20" />
+ <menuitem value="22" label="22" />
+ <menuitem value="24" label="24" />
+ <menuitem value="26" label="26" />
+ <menuitem value="28" label="28" />
+ <menuitem value="30" label="30" />
+ <menuitem value="32" label="32" />
+ <menuitem value="34" label="34" />
+ <menuitem value="36" label="36" />
+ <menuitem value="40" label="40" />
+ <menuitem value="44" label="44" />
+ <menuitem value="48" label="48" />
+ <menuitem value="56" label="56" />
+ <menuitem value="64" label="64" />
+ <menuitem value="72" label="72" />
+ </menupopup>
+ </menulist>
+
+ <!-- serif row -->
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-serif" control="serif" />
+ </hbox>
+ <menulist id="serif" delayprefsave="true" />
+ <spacer />
+ <spacer />
+
+ <!-- sans-serif row -->
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-sans-serif" control="sans-serif" />
+ </hbox>
+ <menulist id="sans-serif" delayprefsave="true" />
+ <spacer />
+ <spacer />
+
+ <!-- monospace row -->
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-monospace" control="monospace" />
+ </hbox>
+ <!--
+ FIXME(emilio): Why is this the only menulist here with crop="end"?
+ This goes back to the beginning of time...
+ -->
+ <menulist id="monospace" crop="end" delayprefsave="true" />
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-monospace-size" control="sizeMono" />
+ </hbox>
+ <menulist id="sizeMono" delayprefsave="true">
+ <menupopup>
+ <menuitem value="9" label="9" />
+ <menuitem value="10" label="10" />
+ <menuitem value="11" label="11" />
+ <menuitem value="12" label="12" />
+ <menuitem value="13" label="13" />
+ <menuitem value="14" label="14" />
+ <menuitem value="15" label="15" />
+ <menuitem value="16" label="16" />
+ <menuitem value="17" label="17" />
+ <menuitem value="18" label="18" />
+ <menuitem value="20" label="20" />
+ <menuitem value="22" label="22" />
+ <menuitem value="24" label="24" />
+ <menuitem value="26" label="26" />
+ <menuitem value="28" label="28" />
+ <menuitem value="30" label="30" />
+ <menuitem value="32" label="32" />
+ <menuitem value="34" label="34" />
+ <menuitem value="36" label="36" />
+ <menuitem value="40" label="40" />
+ <menuitem value="44" label="44" />
+ <menuitem value="48" label="48" />
+ <menuitem value="56" label="56" />
+ <menuitem value="64" label="64" />
+ <menuitem value="72" label="72" />
+ </menupopup>
+ </menulist>
+ </box>
+ <separator class="thin" />
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-minsize" control="minSize" />
+ <menulist id="minSize">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="fonts-minsize-none" />
+ <menuitem value="9" label="9" />
+ <menuitem value="10" label="10" />
+ <menuitem value="11" label="11" />
+ <menuitem value="12" label="12" />
+ <menuitem value="13" label="13" />
+ <menuitem value="14" label="14" />
+ <menuitem value="15" label="15" />
+ <menuitem value="16" label="16" />
+ <menuitem value="17" label="17" />
+ <menuitem value="18" label="18" />
+ <menuitem value="20" label="20" />
+ <menuitem value="22" label="22" />
+ <menuitem value="24" label="24" />
+ <menuitem value="26" label="26" />
+ <menuitem value="28" label="28" />
+ <menuitem value="30" label="30" />
+ <menuitem value="32" label="32" />
+ <menuitem value="34" label="34" />
+ <menuitem value="36" label="36" />
+ <menuitem value="40" label="40" />
+ <menuitem value="44" label="44" />
+ <menuitem value="48" label="48" />
+ <menuitem value="56" label="56" />
+ <menuitem value="64" label="64" />
+ <menuitem value="72" label="72" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <separator />
+ <separator class="groove" />
+ <hbox>
+ <checkbox
+ id="useDocumentFonts"
+ data-l10n-id="fonts-allow-own"
+ preference="browser.display.use_document_fonts"
+ />
+ </hbox>
+ </groupbox>
+
+ <!-- Load the script after the elements for layout issues (bug 1501755). -->
+ <script src="chrome://mozapps/content/preferences/fontbuilder.js" />
+ <script src="chrome://browser/content/preferences/dialogs/fonts.js" />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/handlers.css b/browser/components/preferences/dialogs/handlers.css
new file mode 100644
index 0000000000..b53963fa4d
--- /dev/null
+++ b/browser/components/preferences/dialogs/handlers.css
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Make the icons appear.
+ * Note: we display the icon box for every item whether or not it has an icon
+ * so the labels of all the items align vertically.
+ */
+.actionsMenu > menupopup > menuitem > .menu-iconic-left {
+ display: flex;
+ min-width: 16px;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ #handlersView > richlistitem,
+ .actionsMenu > menupopup > menuitem > .menu-iconic-left {
+ image-rendering: -moz-crisp-edges;
+ }
+}
diff --git a/browser/components/preferences/dialogs/jar.mn b/browser/components/preferences/dialogs/jar.mn
new file mode 100644
index 0000000000..a9f3dd1da2
--- /dev/null
+++ b/browser/components/preferences/dialogs/jar.mn
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+browser.jar:
+ content/browser/preferences/dialogs/addEngine.xhtml
+ content/browser/preferences/dialogs/addEngine.js
+ content/browser/preferences/dialogs/addEngine.css
+ content/browser/preferences/dialogs/applicationManager.xhtml
+ content/browser/preferences/dialogs/applicationManager.js
+ content/browser/preferences/dialogs/blocklists.xhtml
+ content/browser/preferences/dialogs/blocklists.js
+ content/browser/preferences/dialogs/browserLanguages.xhtml
+ content/browser/preferences/dialogs/browserLanguages.js
+ content/browser/preferences/dialogs/clearSiteData.css
+ content/browser/preferences/dialogs/clearSiteData.js
+ content/browser/preferences/dialogs/clearSiteData.xhtml
+ content/browser/preferences/dialogs/colors.xhtml
+ content/browser/preferences/dialogs/colors.js
+ content/browser/preferences/dialogs/connection.xhtml
+ content/browser/preferences/dialogs/connection.js
+ content/browser/preferences/dialogs/dohExceptions.xhtml
+ content/browser/preferences/dialogs/dohExceptions.js
+ content/browser/preferences/dialogs/fonts.xhtml
+ content/browser/preferences/dialogs/fonts.js
+ content/browser/preferences/dialogs/handlers.css
+ content/browser/preferences/dialogs/languages.xhtml
+ content/browser/preferences/dialogs/languages.js
+ content/browser/preferences/dialogs/permissions.xhtml
+ content/browser/preferences/dialogs/sitePermissions.xhtml
+ content/browser/preferences/dialogs/sitePermissions.js
+ content/browser/preferences/dialogs/sitePermissions.css
+ content/browser/preferences/dialogs/containers.xhtml
+ content/browser/preferences/dialogs/containers.js
+ content/browser/preferences/dialogs/permissions.js
+ content/browser/preferences/dialogs/sanitize.xhtml
+ content/browser/preferences/dialogs/sanitize.js
+ content/browser/preferences/dialogs/selectBookmark.xhtml
+ content/browser/preferences/dialogs/selectBookmark.js
+ content/browser/preferences/dialogs/siteDataSettings.xhtml
+ content/browser/preferences/dialogs/siteDataSettings.js
+* content/browser/preferences/dialogs/siteDataRemoveSelected.xhtml
+ content/browser/preferences/dialogs/siteDataRemoveSelected.js
+ content/browser/preferences/dialogs/syncChooseWhatToSync.xhtml
+ content/browser/preferences/dialogs/syncChooseWhatToSync.js
+ content/browser/preferences/dialogs/translationExceptions.xhtml
+ content/browser/preferences/dialogs/translationExceptions.js
+ content/browser/preferences/dialogs/translations.xhtml
+ content/browser/preferences/dialogs/translations.js
diff --git a/browser/components/preferences/dialogs/languages.js b/browser/components/preferences/dialogs/languages.js
new file mode 100644
index 0000000000..502a083c8b
--- /dev/null
+++ b/browser/components/preferences/dialogs/languages.js
@@ -0,0 +1,384 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+document
+ .getElementById("LanguagesDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+Preferences.addAll([
+ { id: "intl.accept_languages", type: "wstring" },
+ { id: "pref.browser.language.disable_button.up", type: "bool" },
+ { id: "pref.browser.language.disable_button.down", type: "bool" },
+ { id: "pref.browser.language.disable_button.remove", type: "bool" },
+ { id: "privacy.spoof_english", type: "int" },
+]);
+
+var gLanguagesDialog = {
+ _availableLanguagesList: [],
+ _acceptLanguages: {},
+
+ _selectedItemID: null,
+
+ onLoad() {
+ let spoofEnglishElement = document.getElementById("spoofEnglish");
+ Preferences.addSyncFromPrefListener(spoofEnglishElement, () =>
+ gLanguagesDialog.readSpoofEnglish()
+ );
+ Preferences.addSyncToPrefListener(spoofEnglishElement, () =>
+ gLanguagesDialog.writeSpoofEnglish()
+ );
+
+ Preferences.get("intl.accept_languages").on("change", () =>
+ this._readAcceptLanguages().catch(console.error)
+ );
+
+ if (!this._availableLanguagesList.length) {
+ document.mozSubdialogReady = this._loadAvailableLanguages();
+ }
+ },
+
+ get _activeLanguages() {
+ return document.getElementById("activeLanguages");
+ },
+
+ get _availableLanguages() {
+ return document.getElementById("availableLanguages");
+ },
+
+ async _loadAvailableLanguages() {
+ // This is a parser for: resource://gre/res/language.properties
+ // The file is formatted like so:
+ // ab[-cd].accept=true|false
+ // ab = language
+ // cd = region
+ var bundleAccepted = document.getElementById("bundleAccepted");
+
+ function LocaleInfo(aLocaleName, aLocaleCode, aIsVisible) {
+ this.name = aLocaleName;
+ this.code = aLocaleCode;
+ this.isVisible = aIsVisible;
+ }
+
+ // 1) Read the available languages out of language.properties
+
+ let localeCodes = [];
+ let localeValues = [];
+ for (let currString of bundleAccepted.strings) {
+ var property = currString.key.split("."); // ab[-cd].accept
+ if (property[1] == "accept") {
+ localeCodes.push(property[0]);
+ localeValues.push(currString.value);
+ }
+ }
+
+ let localeNames = Services.intl.getLocaleDisplayNames(
+ undefined,
+ localeCodes
+ );
+
+ for (let i in localeCodes) {
+ let isVisible =
+ localeValues[i] == "true" &&
+ (!(localeCodes[i] in this._acceptLanguages) ||
+ !this._acceptLanguages[localeCodes[i]]);
+
+ let li = new LocaleInfo(localeNames[i], localeCodes[i], isVisible);
+ this._availableLanguagesList.push(li);
+ }
+
+ await this._buildAvailableLanguageList();
+ await this._readAcceptLanguages();
+ },
+
+ async _buildAvailableLanguageList() {
+ var availableLanguagesPopup = document.getElementById(
+ "availableLanguagesPopup"
+ );
+ while (availableLanguagesPopup.hasChildNodes()) {
+ availableLanguagesPopup.firstChild.remove();
+ }
+
+ let frag = document.createDocumentFragment();
+
+ // Load the UI with the data
+ for (var i = 0; i < this._availableLanguagesList.length; ++i) {
+ let locale = this._availableLanguagesList[i];
+ let localeCode = locale.code;
+ if (
+ locale.isVisible &&
+ (!(localeCode in this._acceptLanguages) ||
+ !this._acceptLanguages[localeCode])
+ ) {
+ var menuitem = document.createXULElement("menuitem");
+ menuitem.id = localeCode;
+ document.l10n.setAttributes(menuitem, "languages-code-format", {
+ locale: locale.name,
+ code: localeCode,
+ });
+ frag.appendChild(menuitem);
+ }
+ }
+
+ await document.l10n.translateFragment(frag);
+
+ // Sort the list of languages by name
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.children);
+
+ items.sort((a, b) => {
+ return comp.compare(a.getAttribute("label"), b.getAttribute("label"));
+ });
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ availableLanguagesPopup.appendChild(frag);
+
+ this._availableLanguages.setAttribute(
+ "label",
+ this._availableLanguages.getAttribute("placeholder")
+ );
+ },
+
+ async _readAcceptLanguages() {
+ while (this._activeLanguages.hasChildNodes()) {
+ this._activeLanguages.firstChild.remove();
+ }
+
+ var selectedIndex = 0;
+ var preference = Preferences.get("intl.accept_languages");
+ if (preference.value == "") {
+ return;
+ }
+ var languages = preference.value.toLowerCase().split(/\s*,\s*/);
+ for (var i = 0; i < languages.length; ++i) {
+ var listitem = document.createXULElement("richlistitem");
+ var label = document.createXULElement("label");
+ listitem.appendChild(label);
+ listitem.id = languages[i];
+ if (languages[i] == this._selectedItemID) {
+ selectedIndex = i;
+ }
+ this._activeLanguages.appendChild(listitem);
+ var localeName = this._getLocaleName(languages[i]);
+ document.l10n.setAttributes(label, "languages-active-code-format", {
+ locale: localeName,
+ code: languages[i],
+ });
+
+ // Hash this language as an "Active" language so we don't
+ // show it in the list that can be added.
+ this._acceptLanguages[languages[i]] = true;
+ }
+
+ // We're forcing an early localization here because otherwise
+ // the initial sizing of the dialog will happen before it and
+ // result in overflow.
+ await document.l10n.translateFragment(this._activeLanguages);
+
+ if (this._activeLanguages.childNodes.length) {
+ this._activeLanguages.ensureIndexIsVisible(selectedIndex);
+ this._activeLanguages.selectedIndex = selectedIndex;
+ }
+
+ // Update states of accept-language list and buttons according to
+ // privacy.resistFingerprinting and privacy.spoof_english.
+ this.readSpoofEnglish();
+ },
+
+ onAvailableLanguageSelect() {
+ var availableLanguages = this._availableLanguages;
+ var addButton = document.getElementById("addButton");
+ addButton.disabled =
+ availableLanguages.disabled || availableLanguages.selectedIndex < 0;
+
+ this._availableLanguages.removeAttribute("accesskey");
+ },
+
+ addLanguage() {
+ var selectedID = this._availableLanguages.selectedItem.id;
+ var preference = Preferences.get("intl.accept_languages");
+ var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/);
+ for (var i = 0; i < arrayOfPrefs.length; ++i) {
+ if (arrayOfPrefs[i] == selectedID) {
+ return;
+ }
+ }
+
+ this._selectedItemID = selectedID;
+
+ if (preference.value == "") {
+ preference.value = selectedID;
+ } else {
+ arrayOfPrefs.unshift(selectedID);
+ preference.value = arrayOfPrefs.join(",");
+ }
+
+ this._acceptLanguages[selectedID] = true;
+ this._availableLanguages.selectedItem = null;
+
+ // Rebuild the available list with the added item removed...
+ this._buildAvailableLanguageList().catch(console.error);
+ },
+
+ removeLanguage() {
+ // Build the new preference value string.
+ var languagesArray = [];
+ for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
+ var item = this._activeLanguages.childNodes[i];
+ if (!item.selected) {
+ languagesArray.push(item.id);
+ } else {
+ this._acceptLanguages[item.id] = false;
+ }
+ }
+ var string = languagesArray.join(",");
+
+ // Get the item to select after the remove operation completes.
+ var selection = this._activeLanguages.selectedItems;
+ var lastSelected = selection[selection.length - 1];
+ var selectItem = lastSelected.nextSibling || lastSelected.previousSibling;
+ selectItem = selectItem ? selectItem.id : null;
+
+ this._selectedItemID = selectItem;
+
+ // Update the preference and force a UI rebuild
+ var preference = Preferences.get("intl.accept_languages");
+ preference.value = string;
+
+ this._buildAvailableLanguageList().catch(console.error);
+ },
+
+ _getLocaleName(localeCode) {
+ if (!this._availableLanguagesList.length) {
+ this._loadAvailableLanguages();
+ }
+ let languageName = "";
+ for (var i = 0; i < this._availableLanguagesList.length; ++i) {
+ if (localeCode == this._availableLanguagesList[i].code) {
+ return this._availableLanguagesList[i].name;
+ }
+ // Try resolving the locale code without region code. Can't return
+ // directly because there might be a perfect match later.
+ if (localeCode.split("-")[0] == this._availableLanguagesList[i].code) {
+ languageName = this._availableLanguagesList[i].name;
+ }
+ }
+
+ return languageName;
+ },
+
+ moveUp() {
+ var selectedItem = this._activeLanguages.selectedItems[0];
+ var previousItem = selectedItem.previousSibling;
+
+ var string = "";
+ for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
+ var item = this._activeLanguages.childNodes[i];
+ string += i == 0 ? "" : ",";
+ if (item.id == previousItem.id) {
+ string += selectedItem.id;
+ } else if (item.id == selectedItem.id) {
+ string += previousItem.id;
+ } else {
+ string += item.id;
+ }
+ }
+
+ this._selectedItemID = selectedItem.id;
+
+ // Update the preference and force a UI rebuild
+ var preference = Preferences.get("intl.accept_languages");
+ preference.value = string;
+ },
+
+ moveDown() {
+ var selectedItem = this._activeLanguages.selectedItems[0];
+ var nextItem = selectedItem.nextSibling;
+
+ var string = "";
+ for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
+ var item = this._activeLanguages.childNodes[i];
+ string += i == 0 ? "" : ",";
+ if (item.id == nextItem.id) {
+ string += selectedItem.id;
+ } else if (item.id == selectedItem.id) {
+ string += nextItem.id;
+ } else {
+ string += item.id;
+ }
+ }
+
+ this._selectedItemID = selectedItem.id;
+
+ // Update the preference and force a UI rebuild
+ var preference = Preferences.get("intl.accept_languages");
+ preference.value = string;
+ },
+
+ onLanguageSelect() {
+ var upButton = document.getElementById("up");
+ var downButton = document.getElementById("down");
+ var removeButton = document.getElementById("remove");
+ switch (this._activeLanguages.selectedCount) {
+ case 0:
+ upButton.disabled = downButton.disabled = removeButton.disabled = true;
+ break;
+ case 1:
+ upButton.disabled = this._activeLanguages.selectedIndex == 0;
+ downButton.disabled =
+ this._activeLanguages.selectedIndex ==
+ this._activeLanguages.childNodes.length - 1;
+ removeButton.disabled = false;
+ break;
+ default:
+ upButton.disabled = true;
+ downButton.disabled = true;
+ removeButton.disabled = false;
+ }
+ },
+
+ readSpoofEnglish() {
+ var checkbox = document.getElementById("spoofEnglish");
+ var resistFingerprinting = Services.prefs.getBoolPref(
+ "privacy.resistFingerprinting"
+ );
+ if (!resistFingerprinting) {
+ checkbox.hidden = true;
+ return false;
+ }
+
+ var spoofEnglish = Preferences.get("privacy.spoof_english").value;
+ var activeLanguages = this._activeLanguages;
+ var availableLanguages = this._availableLanguages;
+ checkbox.hidden = false;
+ switch (spoofEnglish) {
+ case 1: // don't spoof intl.accept_languages
+ activeLanguages.disabled = false;
+ activeLanguages.selectItem(activeLanguages.firstChild);
+ availableLanguages.disabled = false;
+ this.onAvailableLanguageSelect();
+ return false;
+ case 2: // spoof intl.accept_languages
+ activeLanguages.clearSelection();
+ activeLanguages.disabled = true;
+ availableLanguages.disabled = true;
+ this.onAvailableLanguageSelect();
+ return true;
+ default:
+ // will prompt for spoofing intl.accept_languages if resisting fingerprinting
+ return false;
+ }
+ },
+
+ writeSpoofEnglish() {
+ return document.getElementById("spoofEnglish").checked ? 2 : 1;
+ },
+};
diff --git a/browser/components/preferences/dialogs/languages.xhtml b/browser/components/preferences/dialogs/languages.xhtml
new file mode 100644
index 0000000000..bd509e84e4
--- /dev/null
+++ b/browser/components/preferences/dialogs/languages.xhtml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="webpage-languages-window2"
+ data-l10n-attrs="title, style"
+ persist="lastSelected"
+ onload="gLanguagesDialog.onLoad();"
+>
+ <dialog
+ id="LanguagesDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-languages"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/languages.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/languages.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="languages-close-key"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <stringbundleset id="languageSet">
+ <stringbundle
+ id="bundleAccepted"
+ src="resource://gre/res/language.properties"
+ />
+ </stringbundleset>
+
+ <description data-l10n-id="languages-description" />
+ <checkbox
+ id="spoofEnglish"
+ data-l10n-id="languages-customize-spoof-english"
+ preference="privacy.spoof_english"
+ />
+ <box class="languages-grid">
+ <richlistbox
+ id="activeLanguages"
+ seltype="multiple"
+ onselect="gLanguagesDialog.onLanguageSelect();"
+ />
+ <vbox>
+ <button
+ id="up"
+ class="up"
+ oncommand="gLanguagesDialog.moveUp();"
+ disabled="true"
+ data-l10n-id="languages-customize-moveup"
+ preference="pref.browser.language.disable_button.up"
+ />
+ <button
+ id="down"
+ class="down"
+ oncommand="gLanguagesDialog.moveDown();"
+ disabled="true"
+ data-l10n-id="languages-customize-movedown"
+ preference="pref.browser.language.disable_button.down"
+ />
+ <button
+ id="remove"
+ oncommand="gLanguagesDialog.removeLanguage();"
+ disabled="true"
+ data-l10n-id="languages-customize-remove"
+ preference="pref.browser.language.disable_button.remove"
+ />
+ </vbox>
+ <!-- This <vbox> is needed to position search tooltips correctly. -->
+ <vbox>
+ <menulist
+ id="availableLanguages"
+ oncommand="gLanguagesDialog.onAvailableLanguageSelect();"
+ data-l10n-id="languages-customize-select-language"
+ data-l10n-attrs="placeholder"
+ >
+ <menupopup id="availableLanguagesPopup" />
+ </menulist>
+ </vbox>
+ <button
+ id="addButton"
+ class="add-web-language"
+ oncommand="gLanguagesDialog.addLanguage();"
+ disabled="true"
+ data-l10n-id="languages-customize-add"
+ />
+ </box>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/moz.build b/browser/components/preferences/dialogs/moz.build
new file mode 100644
index 0000000000..603c560505
--- /dev/null
+++ b/browser/components/preferences/dialogs/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+for var in ("MOZ_APP_NAME", "MOZ_MACBUNDLE_NAME"):
+ DEFINES[var] = CONFIG[var]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"):
+ DEFINES["HAVE_SHELL_SERVICE"] = 1
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/components/preferences/dialogs/permissions.js b/browser/components/preferences/dialogs/permissions.js
new file mode 100644
index 0000000000..3ca2664611
--- /dev/null
+++ b/browser/components/preferences/dialogs/permissions.js
@@ -0,0 +1,645 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "contentBlockingAllowList",
+ "@mozilla.org/content-blocking-allow-list;1",
+ "nsIContentBlockingAllowList"
+);
+
+const permissionExceptionsL10n = {
+ trackingprotection: {
+ window: "permissions-exceptions-etp-window2",
+ description: "permissions-exceptions-manage-etp-desc",
+ },
+ cookie: {
+ window: "permissions-exceptions-cookie-window2",
+ description: "permissions-exceptions-cookie-desc",
+ },
+ popup: {
+ window: "permissions-exceptions-popup-window2",
+ description: "permissions-exceptions-popup-desc",
+ },
+ "login-saving": {
+ window: "permissions-exceptions-saved-logins-window2",
+ description: "permissions-exceptions-saved-logins-desc",
+ },
+ "https-only-load-insecure": {
+ window: "permissions-exceptions-https-only-window2",
+ description: "permissions-exceptions-https-only-desc",
+ },
+ install: {
+ window: "permissions-exceptions-addons-window2",
+ description: "permissions-exceptions-addons-desc",
+ },
+};
+
+function Permission(principal, type, capability) {
+ this.principal = principal;
+ this.origin = principal.origin;
+ this.type = type;
+ this.capability = capability;
+}
+
+var gPermissionManager = {
+ _type: "",
+ _isObserving: false,
+ _permissions: new Map(),
+ _permissionsToAdd: new Map(),
+ _permissionsToDelete: new Map(),
+ _bundle: null,
+ _list: null,
+ _removeButton: null,
+ _removeAllButton: null,
+
+ onLoad() {
+ let params = window.arguments[0];
+ document.mozSubdialogReady = this.init(params);
+ },
+
+ async init(params) {
+ if (!this._isObserving) {
+ Services.obs.addObserver(this, "perm-changed");
+ this._isObserving = true;
+ }
+
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ this._type = params.permissionType;
+ this._list = document.getElementById("permissionsBox");
+ this._removeButton = document.getElementById("removePermission");
+ this._removeAllButton = document.getElementById("removeAllPermissions");
+
+ this._btnCookieSession = document.getElementById("btnCookieSession");
+ this._btnBlock = document.getElementById("btnBlock");
+ this._btnDisableETP = document.getElementById("btnDisableETP");
+ this._btnAllow = document.getElementById("btnAllow");
+ this._btnHttpsOnlyOff = document.getElementById("btnHttpsOnlyOff");
+ this._btnHttpsOnlyOffTmp = document.getElementById("btnHttpsOnlyOffTmp");
+
+ let permissionsText = document.getElementById("permissionsText");
+
+ let l10n = permissionExceptionsL10n[this._type];
+ document.l10n.setAttributes(permissionsText, l10n.description);
+ document.l10n.setAttributes(document.documentElement, l10n.window);
+
+ let urlFieldVisible =
+ params.blockVisible ||
+ params.sessionVisible ||
+ params.allowVisible ||
+ params.disableETPVisible;
+
+ this._urlField = document.getElementById("url");
+ this._urlField.value = params.prefilledHost;
+ this._urlField.hidden = !urlFieldVisible;
+
+ await document.l10n.translateElements([
+ permissionsText,
+ document.documentElement,
+ ]);
+
+ document.getElementById("btnDisableETP").hidden = !params.disableETPVisible;
+ document.getElementById("btnBlock").hidden = !params.blockVisible;
+ document.getElementById("btnCookieSession").hidden = !(
+ params.sessionVisible && this._type == "cookie"
+ );
+ document.getElementById("btnHttpsOnlyOff").hidden = !(
+ this._type == "https-only-load-insecure"
+ );
+ document.getElementById("btnHttpsOnlyOffTmp").hidden = !(
+ params.sessionVisible && this._type == "https-only-load-insecure"
+ );
+ document.getElementById("btnAllow").hidden = !params.allowVisible;
+
+ this.onHostInput(this._urlField);
+
+ let urlLabel = document.getElementById("urlLabel");
+ urlLabel.hidden = !urlFieldVisible;
+
+ this._hideStatusColumn = params.hideStatusColumn;
+ let statusCol = document.getElementById("statusCol");
+ statusCol.hidden = this._hideStatusColumn;
+ if (this._hideStatusColumn) {
+ statusCol.removeAttribute("data-isCurrentSortCol");
+ document
+ .getElementById("siteCol")
+ .setAttribute("data-isCurrentSortCol", "true");
+ }
+
+ Services.obs.notifyObservers(null, "flush-pending-permissions", this._type);
+
+ this._loadPermissions();
+ this.buildPermissionsList();
+
+ this._urlField.focus();
+ },
+
+ uninit() {
+ if (this._isObserving) {
+ Services.obs.removeObserver(this, "perm-changed");
+ this._isObserving = false;
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic !== "perm-changed") {
+ return;
+ }
+
+ let permission = subject.QueryInterface(Ci.nsIPermission);
+
+ // Ignore unrelated permission types.
+ if (permission.type !== this._type) {
+ return;
+ }
+
+ if (data == "added") {
+ this._addPermissionToList(permission);
+ this.buildPermissionsList();
+ } else if (data == "changed") {
+ let p = this._permissions.get(permission.principal.origin);
+ // Maybe this item has been excluded before because it had an invalid capability.
+ if (p) {
+ p.capability = permission.capability;
+ this._handleCapabilityChange(p);
+ } else {
+ this._addPermissionToList(permission);
+ }
+ this.buildPermissionsList();
+ } else if (data == "deleted") {
+ this._removePermissionFromList(permission.principal.origin);
+ }
+ },
+
+ _handleCapabilityChange(perm) {
+ let permissionlistitem = document.getElementsByAttribute(
+ "origin",
+ perm.origin
+ )[0];
+ document.l10n.setAttributes(
+ permissionlistitem.querySelector(".website-capability-value"),
+ this._getCapabilityL10nId(perm.capability)
+ );
+ },
+
+ _isCapabilitySupported(capability) {
+ return (
+ capability == Ci.nsIPermissionManager.ALLOW_ACTION ||
+ capability == Ci.nsIPermissionManager.DENY_ACTION ||
+ capability == Ci.nsICookiePermission.ACCESS_SESSION ||
+ // Bug 1753600 there are still a few legacy cookies around that have the capability 9,
+ // _getCapabilityL10nId will throw if it receives a capability of 9
+ // that is not in combination with the type https-only-load-insecure
+ (capability ==
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION &&
+ this._type == "https-only-load-insecure")
+ );
+ },
+
+ _getCapabilityL10nId(capability) {
+ // HTTPS-Only Mode phrases exceptions as turning it off
+ if (this._type == "https-only-load-insecure") {
+ return this._getHttpsOnlyCapabilityL10nId(capability);
+ }
+
+ switch (capability) {
+ case Ci.nsIPermissionManager.ALLOW_ACTION:
+ return "permissions-capabilities-listitem-allow";
+ case Ci.nsIPermissionManager.DENY_ACTION:
+ return "permissions-capabilities-listitem-block";
+ case Ci.nsICookiePermission.ACCESS_SESSION:
+ return "permissions-capabilities-listitem-allow-session";
+ default:
+ throw new Error(`Unknown capability: ${capability}`);
+ }
+ },
+
+ _getHttpsOnlyCapabilityL10nId(capability) {
+ switch (capability) {
+ case Ci.nsIPermissionManager.ALLOW_ACTION:
+ return "permissions-capabilities-listitem-off";
+ case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION:
+ return "permissions-capabilities-listitem-off-temporarily";
+ default:
+ throw new Error(`Unknown HTTPS-Only Mode capability: ${capability}`);
+ }
+ },
+
+ _addPermissionToList(perm) {
+ if (perm.type !== this._type) {
+ return;
+ }
+ if (!this._isCapabilitySupported(perm.capability)) {
+ return;
+ }
+
+ // Skip private browsing session permissions.
+ if (
+ perm.principal.privateBrowsingId !==
+ Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID &&
+ perm.expireType === Services.perms.EXPIRE_SESSION
+ ) {
+ return;
+ }
+
+ let p = new Permission(perm.principal, perm.type, perm.capability);
+ this._permissions.set(p.origin, p);
+ },
+
+ _addOrModifyPermission(principal, capability) {
+ // check whether the permission already exists, if not, add it
+ let permissionParams = { principal, type: this._type, capability };
+ let existingPermission = this._permissions.get(principal.origin);
+ if (!existingPermission) {
+ this._permissionsToAdd.set(principal.origin, permissionParams);
+ this._addPermissionToList(permissionParams);
+ this.buildPermissionsList();
+ } else if (existingPermission.capability != capability) {
+ existingPermission.capability = capability;
+ this._permissionsToAdd.set(principal.origin, permissionParams);
+ this._handleCapabilityChange(existingPermission);
+ }
+ },
+
+ _addNewPrincipalToList(list, uri) {
+ list.push(Services.scriptSecurityManager.createContentPrincipal(uri, {}));
+ // If we have ended up with an unknown scheme, the following will throw.
+ list[list.length - 1].origin;
+ },
+
+ addPermission(capability) {
+ let textbox = document.getElementById("url");
+ let input_url = textbox.value.trim(); // trim any leading and trailing space
+ let principals = [];
+ try {
+ // The origin accessor on the principal object will throw if the
+ // principal doesn't have a canonical origin representation. This will
+ // help catch cases where the URI parser parsed something like
+ // `localhost:8080` as having the scheme `localhost`, rather than being
+ // an invalid URI. A canonical origin representation is required by the
+ // permission manager for storage, so this won't prevent any valid
+ // permissions from being entered by the user.
+ try {
+ let uri = Services.io.newURI(input_url);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ if (principal.origin.startsWith("moz-nullprincipal:")) {
+ throw new Error("Null principal");
+ }
+ principals.push(principal);
+ } catch (ex) {
+ this._addNewPrincipalToList(
+ principals,
+ Services.io.newURI("http://" + input_url)
+ );
+ this._addNewPrincipalToList(
+ principals,
+ Services.io.newURI("https://" + input_url)
+ );
+ }
+ } catch (ex) {
+ document.l10n
+ .formatValues([
+ { id: "permissions-invalid-uri-title" },
+ { id: "permissions-invalid-uri-label" },
+ ])
+ .then(([title, message]) => {
+ Services.prompt.alert(window, title, message);
+ });
+ return;
+ }
+ // In case of an ETP exception we compute the contentBlockingAllowList principal
+ // to align with the allow list behavior triggered by the protections panel
+ if (this._type == "trackingprotection") {
+ principals = principals.map(
+ lazy.contentBlockingAllowList.computeContentBlockingAllowListPrincipal
+ );
+ }
+ for (let principal of principals) {
+ this._addOrModifyPermission(principal, capability);
+ }
+
+ textbox.value = "";
+ textbox.focus();
+
+ // covers a case where the site exists already, so the buttons don't disable
+ this.onHostInput(textbox);
+
+ // enable "remove all" button as needed
+ this._setRemoveButtonState();
+ },
+
+ _removePermission(permission) {
+ this._removePermissionFromList(permission.origin);
+
+ // If this permission was added during this session, let's remove
+ // it from the pending adds list to prevent calls to the
+ // permission manager.
+ let isNewPermission = this._permissionsToAdd.delete(permission.origin);
+ if (!isNewPermission) {
+ this._permissionsToDelete.set(permission.origin, permission);
+ }
+ },
+
+ _removePermissionFromList(origin) {
+ this._permissions.delete(origin);
+ let permissionlistitem = document.getElementsByAttribute(
+ "origin",
+ origin
+ )[0];
+ if (permissionlistitem) {
+ permissionlistitem.remove();
+ }
+ },
+
+ _loadPermissions() {
+ // load permissions into a table.
+ for (let nextPermission of Services.perms.all) {
+ this._addPermissionToList(nextPermission);
+ }
+ },
+
+ _createPermissionListItem(permission) {
+ let disabledByPolicy = this._permissionDisabledByPolicy(permission);
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("origin", permission.origin);
+ let row = document.createXULElement("hbox");
+ row.setAttribute("style", "flex: 1");
+
+ let hbox = document.createXULElement("hbox");
+ let website = document.createXULElement("label");
+ website.setAttribute("disabled", disabledByPolicy);
+ website.setAttribute("class", "website-name-value");
+ website.setAttribute("value", permission.origin);
+ hbox.setAttribute("class", "website-name");
+ hbox.setAttribute("style", "flex: 3 3; width: 0");
+ hbox.appendChild(website);
+ row.appendChild(hbox);
+
+ if (!this._hideStatusColumn) {
+ hbox = document.createXULElement("hbox");
+ let capability = document.createXULElement("label");
+ capability.setAttribute("disabled", disabledByPolicy);
+ capability.setAttribute("class", "website-capability-value");
+ document.l10n.setAttributes(
+ capability,
+ this._getCapabilityL10nId(permission.capability)
+ );
+ hbox.setAttribute("class", "website-name");
+ hbox.setAttribute("style", "flex: 1; width: 0");
+ hbox.appendChild(capability);
+ row.appendChild(hbox);
+ }
+
+ richlistitem.appendChild(row);
+ return richlistitem;
+ },
+
+ onWindowKeyPress(event) {
+ // Prevent dialog.js from closing the dialog when the user submits the input
+ // field via the return key.
+ if (
+ event.keyCode == KeyEvent.DOM_VK_RETURN &&
+ document.activeElement == this._urlField
+ ) {
+ event.preventDefault();
+ }
+ },
+
+ onPermissionKeyPress(event) {
+ if (!this._list.selectedItem) {
+ return;
+ }
+
+ if (
+ event.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ this.onPermissionDelete();
+ event.preventDefault();
+ }
+ },
+
+ onHostKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
+ if (!document.getElementById("btnAllow").hidden) {
+ document.getElementById("btnAllow").click();
+ } else if (!document.getElementById("btnBlock").hidden) {
+ document.getElementById("btnBlock").click();
+ } else if (!document.getElementById("btnHttpsOnlyOff").hidden) {
+ document.getElementById("btnHttpsOnlyOff").click();
+ } else if (!document.getElementById("btnDisableETP").hidden) {
+ document.getElementById("btnDisableETP").click();
+ }
+ }
+ },
+
+ onHostInput(siteField) {
+ this._btnCookieSession.disabled =
+ this._btnCookieSession.hidden || !siteField.value;
+ this._btnHttpsOnlyOff.disabled =
+ this._btnHttpsOnlyOff.hidden || !siteField.value;
+ this._btnHttpsOnlyOffTmp.disabled =
+ this._btnHttpsOnlyOffTmp.hidden || !siteField.value;
+ this._btnBlock.disabled = this._btnBlock.hidden || !siteField.value;
+ this._btnDisableETP.disabled =
+ this._btnDisableETP.hidden || !siteField.value;
+ this._btnAllow.disabled = this._btnAllow.hidden || !siteField.value;
+ },
+
+ _setRemoveButtonState() {
+ if (!this._list) {
+ return;
+ }
+
+ let hasSelection = this._list.selectedIndex >= 0;
+
+ let disabledByPolicy = false;
+ if (Services.policies.status === Services.policies.ACTIVE && hasSelection) {
+ let origin = this._list.selectedItem.getAttribute("origin");
+ disabledByPolicy = this._permissionDisabledByPolicy(
+ this._permissions.get(origin)
+ );
+ }
+
+ this._removeButton.disabled = !hasSelection || disabledByPolicy;
+ let disabledItems = this._list.querySelectorAll(
+ "label.website-name-value[disabled='true']"
+ );
+
+ this._removeAllButton.disabled =
+ this._list.itemCount == disabledItems.length;
+ },
+
+ onPermissionDelete() {
+ let richlistitem = this._list.selectedItem;
+ let origin = richlistitem.getAttribute("origin");
+ let permission = this._permissions.get(origin);
+ if (this._permissionDisabledByPolicy(permission)) {
+ return;
+ }
+
+ this._removePermission(permission);
+
+ this._setRemoveButtonState();
+ },
+
+ onAllPermissionsDelete() {
+ for (let permission of this._permissions.values()) {
+ if (this._permissionDisabledByPolicy(permission)) {
+ continue;
+ }
+ this._removePermission(permission);
+ }
+
+ this._setRemoveButtonState();
+ },
+
+ onPermissionSelect() {
+ this._setRemoveButtonState();
+ },
+
+ onApplyChanges() {
+ // Stop observing permission changes since we are about
+ // to write out the pending adds/deletes and don't need
+ // to update the UI
+ this.uninit();
+
+ for (let p of this._permissionsToDelete.values()) {
+ Services.perms.removeFromPrincipal(p.principal, p.type);
+ }
+
+ for (let p of this._permissionsToAdd.values()) {
+ // If this sets the HTTPS-Only exemption only for this
+ // session, then the expire-type has to be set.
+ if (
+ p.capability ==
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
+ ) {
+ Services.perms.addFromPrincipal(
+ p.principal,
+ p.type,
+ p.capability,
+ Ci.nsIPermissionManager.EXPIRE_SESSION
+ );
+ } else {
+ Services.perms.addFromPrincipal(p.principal, p.type, p.capability);
+ }
+ }
+ },
+
+ buildPermissionsList(sortCol) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+ let frag = document.createDocumentFragment();
+
+ let permissions = Array.from(this._permissions.values());
+
+ for (let permission of permissions) {
+ let richlistitem = this._createPermissionListItem(permission);
+ frag.appendChild(richlistitem);
+ }
+
+ // Sort permissions.
+ this._sortPermissions(this._list, frag, sortCol);
+
+ this._list.appendChild(frag);
+
+ this._setRemoveButtonState();
+ },
+
+ _permissionDisabledByPolicy(permission) {
+ let permissionObject = Services.perms.getPermissionObject(
+ permission.principal,
+ this._type,
+ false
+ );
+ return (
+ permissionObject?.expireType == Ci.nsIPermissionManager.EXPIRE_POLICY
+ );
+ },
+
+ _sortPermissions(list, frag, column) {
+ let sortDirection;
+
+ if (!column) {
+ column = document.querySelector("treecol[data-isCurrentSortCol=true]");
+ sortDirection =
+ column.getAttribute("data-last-sortDirection") || "ascending";
+ } else {
+ sortDirection = column.getAttribute("data-last-sortDirection");
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = null;
+ switch (column.id) {
+ case "siteCol":
+ sortFunc = (a, b) => {
+ return comp.compare(
+ a.getAttribute("origin"),
+ b.getAttribute("origin")
+ );
+ };
+ break;
+
+ case "statusCol":
+ sortFunc = (a, b) => {
+ // The capabilities values ("Allow" and "Block") are localized asynchronously.
+ // Sort based on the guaranteed-present localization ID instead, note that the
+ // ascending/descending arrow may be pointing the wrong way.
+ return (
+ a
+ .querySelector(".website-capability-value")
+ .getAttribute("data-l10n-id") >
+ b
+ .querySelector(".website-capability-value")
+ .getAttribute("data-l10n-id")
+ );
+ };
+ break;
+ }
+
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.querySelectorAll("richlistitem"));
+
+ if (sortDirection === "descending") {
+ items.sort((a, b) => sortFunc(b, a));
+ } else {
+ items.sort(sortFunc);
+ }
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ let cols = list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("data-isCurrentSortCol");
+ c.removeAttribute("sortDirection");
+ });
+ column.setAttribute("data-isCurrentSortCol", "true");
+ column.setAttribute("sortDirection", sortDirection);
+ column.setAttribute("data-last-sortDirection", sortDirection);
+ },
+};
diff --git a/browser/components/preferences/dialogs/permissions.xhtml b/browser/components/preferences/dialogs/permissions.xhtml
new file mode 100644
index 0000000000..13d756bae0
--- /dev/null
+++ b/browser/components/preferences/dialogs/permissions.xhtml
@@ -0,0 +1,134 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="PermissionsDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="permissions-window2"
+ data-l10n-attrs="title, style"
+ onload="gPermissionManager.onLoad();"
+ onunload="gPermissionManager.uninit();"
+ persist="width height"
+ onkeypress="gPermissionManager.onWindowKeyPress(event);"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="permission-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/permissions.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/permissions.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="permissions-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description id="permissionsText" control="url" />
+ <separator class="thin" />
+ <label id="urlLabel" control="url" data-l10n-id="permissions-address" />
+ <hbox align="start">
+ <html:input
+ id="url"
+ type="text"
+ style="flex: 1"
+ oninput="gPermissionManager.onHostInput(event.target);"
+ onkeypress="gPermissionManager.onHostKeyPress(event);"
+ />
+ </hbox>
+ <hbox pack="end">
+ <button
+ id="btnDisableETP"
+ disabled="true"
+ data-l10n-id="permissions-disable-etp"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"
+ />
+ <button
+ id="btnBlock"
+ disabled="true"
+ data-l10n-id="permissions-block"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);"
+ />
+ <button
+ id="btnCookieSession"
+ disabled="true"
+ data-l10n-id="permissions-session"
+ oncommand="gPermissionManager.addPermission(Ci.nsICookiePermission.ACCESS_SESSION);"
+ />
+ <button
+ id="btnAllow"
+ disabled="true"
+ data-l10n-id="permissions-allow"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"
+ />
+ <button
+ id="btnHttpsOnlyOff"
+ disabled="true"
+ data-l10n-id="permissions-button-off"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"
+ />
+ <button
+ id="btnHttpsOnlyOffTmp"
+ disabled="true"
+ data-l10n-id="permissions-button-off-temporarily"
+ oncommand="gPermissionManager.addPermission(Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION);"
+ />
+ </hbox>
+ <separator class="thin" />
+ <listheader>
+ <treecol
+ id="siteCol"
+ data-l10n-id="permissions-site-name"
+ style="flex: 3 3 auto; width: 0"
+ onclick="gPermissionManager.buildPermissionsList(event.target)"
+ />
+ <treecol
+ id="statusCol"
+ data-l10n-id="permissions-status"
+ style="flex: 1 1 auto; width: 0"
+ data-isCurrentSortCol="true"
+ onclick="gPermissionManager.buildPermissionsList(event.target);"
+ />
+ </listheader>
+ <richlistbox
+ id="permissionsBox"
+ selected="false"
+ onkeypress="gPermissionManager.onPermissionKeyPress(event);"
+ onselect="gPermissionManager.onPermissionSelect();"
+ />
+ </vbox>
+
+ <hbox class="actionButtons">
+ <button
+ id="removePermission"
+ disabled="true"
+ data-l10n-id="permissions-remove"
+ oncommand="gPermissionManager.onPermissionDelete();"
+ />
+ <button
+ id="removeAllPermissions"
+ data-l10n-id="permissions-remove-all"
+ oncommand="gPermissionManager.onAllPermissionsDelete();"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/sanitize.js b/browser/components/preferences/dialogs/sanitize.js
new file mode 100644
index 0000000000..37f818e011
--- /dev/null
+++ b/browser/components/preferences/dialogs/sanitize.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+document
+ .querySelector("dialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+Preferences.addAll([
+ { id: "privacy.clearOnShutdown.history", type: "bool" },
+ { id: "privacy.clearOnShutdown.formdata", type: "bool" },
+ { id: "privacy.clearOnShutdown.downloads", type: "bool" },
+ { id: "privacy.clearOnShutdown.cookies", type: "bool" },
+ { id: "privacy.clearOnShutdown.cache", type: "bool" },
+ { id: "privacy.clearOnShutdown.offlineApps", type: "bool" },
+ { id: "privacy.clearOnShutdown.sessions", type: "bool" },
+ { id: "privacy.clearOnShutdown.siteSettings", type: "bool" },
+]);
+
+var gSanitizeDialog = Object.freeze({
+ init() {
+ this.onClearHistoryChanged();
+
+ Preferences.get("privacy.clearOnShutdown.history").on(
+ "change",
+ this.onClearHistoryChanged.bind(this)
+ );
+ },
+
+ onClearHistoryChanged() {
+ let downloadsPref = Preferences.get("privacy.clearOnShutdown.downloads");
+ let historyPref = Preferences.get("privacy.clearOnShutdown.history");
+ downloadsPref.value = historyPref.value;
+ },
+});
diff --git a/browser/components/preferences/dialogs/sanitize.xhtml b/browser/components/preferences/dialogs/sanitize.xhtml
new file mode 100644
index 0000000000..40f030d8ab
--- /dev/null
+++ b/browser/components/preferences/dialogs/sanitize.xhtml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+
+<!DOCTYPE window>
+
+<window
+ id="SanitizeDialog"
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="lastSelected"
+ data-l10n-id="sanitize-prefs2"
+ data-l10n-attrs="style"
+ onload="gSanitizeDialog.init();"
+>
+ <dialog buttons="accept,cancel,help" helpTopic="prefs-clear-private-data">
+ <linkset>
+ <html:link rel="localization" href="browser/sanitize.ftl" />
+ <html:link rel="localization" href="branding/brand.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="window-close"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <script src="chrome://browser/content/preferences/dialogs/sanitize.js" />
+
+ <description data-l10n-id="clear-data-settings-label"></description>
+
+ <groupbox>
+ <label><html:h2 data-l10n-id="history-section-label" /></label>
+ <hbox>
+ <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+ <checkbox
+ data-l10n-id="item-history-and-downloads"
+ preference="privacy.clearOnShutdown.history"
+ />
+ <checkbox
+ data-l10n-id="item-active-logins"
+ preference="privacy.clearOnShutdown.sessions"
+ />
+ <checkbox
+ data-l10n-id="item-form-search-history"
+ preference="privacy.clearOnShutdown.formdata"
+ />
+ </vbox>
+ <vbox>
+ <checkbox
+ data-l10n-id="item-cookies"
+ preference="privacy.clearOnShutdown.cookies"
+ />
+ <checkbox
+ data-l10n-id="item-cache"
+ preference="privacy.clearOnShutdown.cache"
+ />
+ </vbox>
+ </hbox>
+ </groupbox>
+ <groupbox>
+ <label><html:h2 data-l10n-id="data-section-label" /></label>
+ <hbox>
+ <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+ <checkbox
+ data-l10n-id="item-site-settings"
+ preference="privacy.clearOnShutdown.siteSettings"
+ />
+ </vbox>
+ <vbox flex="1">
+ <checkbox
+ data-l10n-id="item-offline-apps"
+ preference="privacy.clearOnShutdown.offlineApps"
+ />
+ </vbox>
+ </hbox>
+ </groupbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/selectBookmark.js b/browser/components/preferences/dialogs/selectBookmark.js
new file mode 100644
index 0000000000..5fe4c645e7
--- /dev/null
+++ b/browser/components/preferences/dialogs/selectBookmark.js
@@ -0,0 +1,119 @@
+//* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Shared Places Import - change other consumers if you change this: */
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs",
+ PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "PlacesTreeView",
+ "chrome://browser/content/places/treeView.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
+ "chrome://browser/content/places/controller.js"
+);
+/* End Shared Places Import */
+
+/**
+ * SelectBookmarkDialog controls the user interface for the "Use Bookmark for
+ * Home Page" dialog.
+ *
+ * The caller (gMainPane.setHomePageToBookmark in main.js) invokes this dialog
+ * with a single argument - a reference to an object with a .urls property and
+ * a .names property. This dialog is responsible for updating the contents of
+ * the .urls property with an array of URLs to use as home pages and for
+ * updating the .names property with an array of names for those URLs before it
+ * closes.
+ */
+var SelectBookmarkDialog = {
+ init: function SBD_init() {
+ document.getElementById("bookmarks").place =
+ "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY;
+
+ // Initial update of the OK button.
+ this.selectionChanged();
+ document.addEventListener("dialogaccept", function () {
+ SelectBookmarkDialog.accept();
+ });
+ },
+
+ /**
+ * Update the disabled state of the OK button as the user changes the
+ * selection within the view.
+ */
+ selectionChanged: function SBD_selectionChanged() {
+ var accept = document
+ .getElementById("selectBookmarkDialog")
+ .getButton("accept");
+ var bookmarks = document.getElementById("bookmarks");
+ var disableAcceptButton = true;
+ if (bookmarks.hasSelection) {
+ if (!PlacesUtils.nodeIsSeparator(bookmarks.selectedNode)) {
+ disableAcceptButton = false;
+ }
+ }
+ accept.disabled = disableAcceptButton;
+ },
+
+ onItemDblClick: function SBD_onItemDblClick() {
+ var bookmarks = document.getElementById("bookmarks");
+ var selectedNode = bookmarks.selectedNode;
+ if (selectedNode && PlacesUtils.nodeIsURI(selectedNode)) {
+ /**
+ * The user has double clicked on a tree row that is a link. Take this to
+ * mean that they want that link to be their homepage, and close the dialog.
+ */
+ document
+ .getElementById("selectBookmarkDialog")
+ .getButton("accept")
+ .click();
+ }
+ },
+
+ /**
+ * User accepts their selection. Set all the selected URLs or the contents
+ * of the selected folder as the list of homepages.
+ */
+ accept: function SBD_accept() {
+ var bookmarks = document.getElementById("bookmarks");
+ if (!bookmarks.hasSelection) {
+ throw new Error(
+ "Should not be able to accept dialog if there is no selected URL!"
+ );
+ }
+ var urls = [];
+ var names = [];
+ var selectedNode = bookmarks.selectedNode;
+ if (PlacesUtils.nodeIsFolder(selectedNode)) {
+ let concreteGuid = PlacesUtils.getConcreteItemGuid(selectedNode);
+ var contents = PlacesUtils.getFolderContents(concreteGuid).root;
+ var cc = contents.childCount;
+ for (var i = 0; i < cc; ++i) {
+ var node = contents.getChild(i);
+ if (PlacesUtils.nodeIsURI(node)) {
+ urls.push(node.uri);
+ names.push(node.title);
+ }
+ }
+ contents.containerOpen = false;
+ } else {
+ urls.push(selectedNode.uri);
+ names.push(selectedNode.title);
+ }
+ window.arguments[0].urls = urls;
+ window.arguments[0].names = names;
+ },
+};
diff --git a/browser/components/preferences/dialogs/selectBookmark.xhtml b/browser/components/preferences/dialogs/selectBookmark.xhtml
new file mode 100644
index 0000000000..3a46394328
--- /dev/null
+++ b/browser/components/preferences/dialogs/selectBookmark.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="select-bookmark-window2"
+ data-l10n-attrs="title, style"
+ persist="width height"
+ onload="SelectBookmarkDialog.init();"
+>
+ <dialog id="selectBookmarkDialog">
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/selectBookmark.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/selectBookmark.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://browser/content/places/places-tree.js" />
+
+ <description data-l10n-id="select-bookmark-desc" />
+
+ <separator class="thin" />
+
+ <tree
+ id="bookmarks"
+ flex="1"
+ is="places-tree"
+ style="height: 15em"
+ hidecolumnpicker="true"
+ seltype="single"
+ ondblclick="SelectBookmarkDialog.onItemDblClick();"
+ onselect="SelectBookmarkDialog.selectionChanged();"
+ disableUserActions="true"
+ >
+ <treecols>
+ <treecol id="title" flex="1" primary="true" hideheader="true" />
+ </treecols>
+ <treechildren id="bookmarksChildren" flex="1" />
+ </tree>
+
+ <separator class="thin" />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/siteDataRemoveSelected.js b/browser/components/preferences/dialogs/siteDataRemoveSelected.js
new file mode 100644
index 0000000000..e722a2d826
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This dialog will ask the user to confirm that they really want to delete all
+ * site data for a number of hosts.
+ **/
+let gSiteDataRemoveSelected = {
+ init() {
+ document.addEventListener("dialogaccept", function () {
+ window.arguments[0].allowed = true;
+ });
+ document.addEventListener("dialogcancel", function () {
+ window.arguments[0].allowed = false;
+ });
+
+ let list = document.getElementById("removalList");
+
+ let hosts = window.arguments[0].hosts;
+
+ if (!hosts) {
+ throw new Error("Must specify hosts option in arguments.");
+ }
+ let dialog = document.getElementById("SiteDataRemoveSelectedDialog");
+ if (hosts.length == 1) {
+ dialog.classList.add("single-entry");
+ document.l10n.setAttributes(
+ document.getElementById("removing-description"),
+ "site-data-removing-single-desc",
+ {
+ baseDomain: hosts[0],
+ }
+ );
+ return;
+ }
+ dialog.classList.add("multi-entry");
+ hosts.sort();
+ let fragment = document.createDocumentFragment();
+ for (let host of hosts) {
+ let listItem = document.createXULElement("richlistitem");
+ let label = document.createXULElement("label");
+ if (host) {
+ label.setAttribute("value", host);
+ } else {
+ document.l10n.setAttributes(label, "site-data-local-file-host");
+ }
+ listItem.appendChild(label);
+ fragment.appendChild(listItem);
+ }
+ list.appendChild(fragment);
+ },
+};
diff --git a/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml
new file mode 100644
index 0000000000..56b50f3d53
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/siteDataSettings.css" type="text/css"?>
+
+<window id="SiteDataRemoveSelectedDialog"
+ width="500"
+ data-l10n-id="site-data-removing-dialog"
+ data-l10n-attrs="title"
+ onload="gSiteDataRemoveSelected.init();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<dialog data-l10n-id="site-data-removing-dialog"
+ data-l10n-attrs="buttonlabelaccept">
+
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
+ </linkset>
+
+ <hbox>
+ <vbox>
+ <image class="question-icon"/>
+ </vbox>
+ <vbox flex="1">
+ <!-- Only show this label on OS X because of no dialog title -->
+ <label id="removing-label"
+ data-l10n-id="site-data-removing-header"
+#ifndef XP_MACOSX
+ hidden="true"
+#endif
+ />
+ <separator class="thin"/>
+ <description id="removing-description" data-l10n-id="site-data-removing-desc"/>
+ </vbox>
+ </hbox>
+ <separator class="multi-site"/>
+
+ <label data-l10n-id="site-data-removing-table" class="multi-site"/>
+ <separator class="thin multi-site"/>
+ <richlistbox id="removalList" class="theme-listbox multi-site" flex="1"/>
+ <!-- Load the script after the elements for layout issues (bug 1501755). -->
+ <script src="chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.js"/>
+</dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/siteDataSettings.js b/browser/components/preferences/dialogs/siteDataSettings.js
new file mode 100644
index 0000000000..7c0c490e5d
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataSettings.js
@@ -0,0 +1,335 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "SiteDataManager",
+ "resource:///modules/SiteDataManager.jsm"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+});
+
+let gSiteDataSettings = {
+ // Array of metadata of sites. Each array element is object holding:
+ // - uri: uri of site; instance of nsIURI
+ // - baseDomain: base domain of the site
+ // - cookies: array of cookies of that site
+ // - usage: disk usage which site uses
+ // - userAction: "remove" or "update-permission"; the action user wants to take.
+ _sites: null,
+
+ _list: null,
+ _searchBox: null,
+
+ _createSiteListItem(site) {
+ let item = document.createXULElement("richlistitem");
+ item.setAttribute("host", site.baseDomain);
+ let container = document.createXULElement("hbox");
+
+ // Creates a new column item with the specified relative width.
+ function addColumnItem(l10n, flexWidth, tooltipText) {
+ let box = document.createXULElement("hbox");
+ box.className = "item-box";
+ box.setAttribute("style", `flex: ${flexWidth} ${flexWidth};`);
+ let label = document.createXULElement("label");
+ label.setAttribute("crop", "end");
+ if (l10n) {
+ if (l10n.hasOwnProperty("raw")) {
+ box.setAttribute("tooltiptext", l10n.raw);
+ label.setAttribute("value", l10n.raw);
+ } else {
+ document.l10n.setAttributes(label, l10n.id, l10n.args);
+ }
+ }
+ if (tooltipText) {
+ box.setAttribute("tooltiptext", tooltipText);
+ }
+ box.appendChild(label);
+ container.appendChild(box);
+ }
+
+ // Add "Host" column.
+ let hostData = site.baseDomain
+ ? { raw: site.baseDomain }
+ : { id: "site-data-local-file-host" };
+ addColumnItem(hostData, "4");
+
+ // Add "Cookies" column.
+ addColumnItem({ raw: site.cookies.length }, "1");
+
+ // Add "Storage" column
+ if (site.usage > 0 || site.persisted) {
+ let [value, unit] = DownloadUtils.convertByteUnits(site.usage);
+ let strName = site.persisted
+ ? "site-storage-persistent"
+ : "site-storage-usage";
+ addColumnItem(
+ {
+ id: strName,
+ args: { value, unit },
+ },
+ "2"
+ );
+ } else {
+ // Pass null to avoid showing "0KB" when there is no site data stored.
+ addColumnItem(null, "2");
+ }
+
+ // Add "Last Used" column.
+ let formattedLastAccessed =
+ site.lastAccessed > 0
+ ? this._relativeTimeFormat.formatBestUnit(site.lastAccessed)
+ : null;
+ let formattedFullDate =
+ site.lastAccessed > 0
+ ? this._absoluteTimeFormat.format(site.lastAccessed)
+ : null;
+ addColumnItem(
+ site.lastAccessed > 0 ? { raw: formattedLastAccessed } : null,
+ "2",
+ formattedFullDate
+ );
+
+ item.appendChild(container);
+ return item;
+ },
+
+ init() {
+ function setEventListener(id, eventType, callback) {
+ document
+ .getElementById(id)
+ .addEventListener(eventType, callback.bind(gSiteDataSettings));
+ }
+
+ this._absoluteTimeFormat = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "short",
+ });
+
+ this._relativeTimeFormat = new Services.intl.RelativeTimeFormat(
+ undefined,
+ {}
+ );
+
+ this._list = document.getElementById("sitesList");
+ this._searchBox = document.getElementById("searchBox");
+ SiteDataManager.getSites().then(sites => {
+ this._sites = sites;
+ let sortCol = document.querySelector(
+ "treecol[data-isCurrentSortCol=true]"
+ );
+ this._sortSites(this._sites, sortCol);
+ this._buildSitesList(this._sites);
+ Services.obs.notifyObservers(null, "sitedata-settings-init");
+ });
+
+ setEventListener("sitesList", "select", this.onSelect);
+ setEventListener("hostCol", "click", this.onClickTreeCol);
+ setEventListener("usageCol", "click", this.onClickTreeCol);
+ setEventListener("lastAccessedCol", "click", this.onClickTreeCol);
+ setEventListener("cookiesCol", "click", this.onClickTreeCol);
+ setEventListener("searchBox", "command", this.onCommandSearch);
+ setEventListener("removeAll", "command", this.onClickRemoveAll);
+ setEventListener("removeSelected", "command", this.removeSelected);
+
+ document.addEventListener("dialogaccept", e => this.saveChanges(e));
+ },
+
+ _updateButtonsState() {
+ let items = this._list.getElementsByTagName("richlistitem");
+ let removeSelectedBtn = document.getElementById("removeSelected");
+ let removeAllBtn = document.getElementById("removeAll");
+ removeSelectedBtn.disabled = !this._list.selectedItems.length;
+ removeAllBtn.disabled = !items.length;
+
+ let l10nId = this._searchBox.value
+ ? "site-data-remove-shown"
+ : "site-data-remove-all";
+ document.l10n.setAttributes(removeAllBtn, l10nId);
+ },
+
+ /**
+ * @param sites {Array}
+ * @param col {XULElement} the <treecol> being sorted on
+ */
+ _sortSites(sites, col) {
+ let isCurrentSortCol = col.getAttribute("data-isCurrentSortCol");
+ let sortDirection =
+ col.getAttribute("data-last-sortDirection") || "ascending";
+ if (isCurrentSortCol) {
+ // Sort on the current column, flip the sorting direction
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = null;
+ switch (col.id) {
+ case "hostCol":
+ sortFunc = (a, b) => {
+ let aHost = a.baseDomain.toLowerCase();
+ let bHost = b.baseDomain.toLowerCase();
+ return aHost.localeCompare(bHost);
+ };
+ break;
+
+ case "cookiesCol":
+ sortFunc = (a, b) => a.cookies.length - b.cookies.length;
+ break;
+
+ case "usageCol":
+ sortFunc = (a, b) => a.usage - b.usage;
+ break;
+
+ case "lastAccessedCol":
+ sortFunc = (a, b) => a.lastAccessed - b.lastAccessed;
+ break;
+ }
+ if (sortDirection === "descending") {
+ sites.sort((a, b) => sortFunc(b, a));
+ } else {
+ sites.sort(sortFunc);
+ }
+
+ let cols = this._list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("sortDirection");
+ c.removeAttribute("data-isCurrentSortCol");
+ });
+ col.setAttribute("data-isCurrentSortCol", true);
+ col.setAttribute("sortDirection", sortDirection);
+ col.setAttribute("data-last-sortDirection", sortDirection);
+ },
+
+ /**
+ * @param sites {Array} array of metadata of sites
+ */
+ _buildSitesList(sites) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+
+ let keyword = this._searchBox.value.toLowerCase().trim();
+ let fragment = document.createDocumentFragment();
+ for (let site of sites) {
+ if (keyword && !site.baseDomain.includes(keyword)) {
+ continue;
+ }
+
+ if (site.userAction === "remove") {
+ continue;
+ }
+
+ let item = this._createSiteListItem(site);
+ fragment.appendChild(item);
+ }
+ this._list.appendChild(fragment);
+ this._updateButtonsState();
+ },
+
+ _removeSiteItems(items) {
+ for (let i = items.length - 1; i >= 0; --i) {
+ let item = items[i];
+ let baseDomain = item.getAttribute("host");
+ let siteForBaseDomain = this._sites.find(
+ site => site.baseDomain == baseDomain
+ );
+ if (siteForBaseDomain) {
+ siteForBaseDomain.userAction = "remove";
+ }
+ item.remove();
+ }
+ this._updateButtonsState();
+ },
+
+ async saveChanges(event) {
+ let removals = this._sites
+ .filter(site => site.userAction == "remove")
+ .map(site => site.baseDomain);
+
+ if (removals.length) {
+ let removeAll = removals.length == this._sites.length;
+ let promptArg = removeAll ? undefined : removals;
+ if (!SiteDataManager.promptSiteDataRemoval(window, promptArg)) {
+ // If the user cancelled the confirm dialog keep the site data window open,
+ // they can still press cancel again to exit.
+ event.preventDefault();
+ return;
+ }
+ try {
+ if (removeAll) {
+ await SiteDataManager.removeAll();
+ } else {
+ await SiteDataManager.remove(removals);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ },
+
+ removeSelected() {
+ let lastIndex = this._list.selectedItems.length - 1;
+ let lastSelectedItem = this._list.selectedItems[lastIndex];
+ let lastSelectedItemPosition = this._list.getIndexOfItem(lastSelectedItem);
+ let nextSelectedItem = this._list.getItemAtIndex(
+ lastSelectedItemPosition + 1
+ );
+
+ this._removeSiteItems(this._list.selectedItems);
+ this._list.clearSelection();
+
+ if (nextSelectedItem) {
+ this._list.selectedItem = nextSelectedItem;
+ } else {
+ this._list.selectedIndex = this._list.itemCount - 1;
+ }
+ },
+
+ onClickTreeCol(e) {
+ this._sortSites(this._sites, e.target);
+ this._buildSitesList(this._sites);
+ this._list.clearSelection();
+ },
+
+ onCommandSearch() {
+ this._buildSitesList(this._sites);
+ this._list.clearSelection();
+ },
+
+ onClickRemoveAll() {
+ let siteItems = this._list.getElementsByTagName("richlistitem");
+ if (siteItems.length) {
+ this._removeSiteItems(siteItems);
+ }
+ },
+
+ onKeyPress(e) {
+ if (
+ e.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ if (!e.target.closest("#sitesList")) {
+ // The user is typing or has not selected an item from the list to remove
+ return;
+ }
+ // The users intention is to delete site data
+ this.removeSelected();
+ }
+ },
+
+ onSelect() {
+ this._updateButtonsState();
+ },
+};
diff --git a/browser/components/preferences/dialogs/siteDataSettings.xhtml b/browser/components/preferences/dialogs/siteDataSettings.xhtml
new file mode 100644
index 0000000000..251b70d3fe
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataSettings.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/siteDataSettings.css" type="text/css"?>
+
+<window
+ id="SiteDataSettingsDialog"
+ data-l10n-id="site-data-settings-window"
+ data-l10n-attrs="title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="min-width: 45em"
+ onload="gSiteDataSettings.init();"
+ onkeypress="gSiteDataSettings.onKeyPress(event);"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="site-data-settings-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/siteDataSettings.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/siteDataSettings.js" />
+
+ <vbox flex="1" class="contentPane">
+ <description
+ id="settingsDescription"
+ data-l10n-id="site-data-settings-description"
+ />
+ <separator class="thin" />
+
+ <hbox id="searchBoxContainer">
+ <search-textbox
+ id="searchBox"
+ flex="1"
+ data-l10n-id="site-data-search-textbox"
+ data-l10n-attrs="placeholder"
+ />
+ </hbox>
+ <separator class="thin" />
+
+ <listheader>
+ <treecol
+ style="flex: 4 4 auto; width: 50px"
+ data-l10n-id="site-data-column-host"
+ id="hostCol"
+ />
+ <treecol
+ style="flex: 1 auto; width: 50px"
+ data-l10n-id="site-data-column-cookies"
+ id="cookiesCol"
+ />
+ <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
+ <treecol
+ style="flex: 2 2 auto; width: 50px"
+ data-l10n-id="site-data-column-storage"
+ id="usageCol"
+ data-isCurrentSortCol="true"
+ />
+ <treecol
+ style="flex: 2 2 auto; width: 50px"
+ data-l10n-id="site-data-column-last-used"
+ id="lastAccessedCol"
+ />
+ </listheader>
+ <richlistbox seltype="multiple" id="sitesList" orient="vertical" />
+ </vbox>
+
+ <hbox align="start">
+ <button id="removeSelected" data-l10n-id="site-data-remove-selected" />
+ <button id="removeAll" />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/sitePermissions.css b/browser/components/preferences/dialogs/sitePermissions.css
new file mode 100644
index 0000000000..e41c7203fe
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.css
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.website-name,
+hbox.website-status {
+ overflow: hidden; /* Allows equal sizing combined with width="0" */
+ padding-inline-start: 7px;
+ align-items: center;
+}
+
+#permissionsBox {
+ flex: 1 auto;
+ height: 18em;
+ min-height: 70px; /* 2 * 35px, which is the min row height specified below */
+}
+
+#siteCol,
+#statusCol,
+#permissionsBox > richlistitem {
+ min-height: 35px;
+}
+
+#permissionsBox > richlistitem > hbox {
+ flex: 1;
+}
+
+#siteCol {
+ flex: 3 3 auto;
+}
+
+.website-name {
+ flex: 3 3;
+}
+
+#statusCol {
+ flex: 1 auto;
+}
+
+.website-status {
+ flex: 1;
+}
+
+/* TODO(bug 1802993): Seems this could be on .website-name instead of label? */
+#siteCol,
+#statusCol,
+.website-name > label,
+.website-status {
+ width: 75px;
+}
+
+menulist.website-status {
+ margin: 1px;
+ margin-inline-end: 5px;
+}
+
+#browserNotificationsPermissionExtensionContent,
+#permissionsDisableDescription {
+ margin-inline-start: 32px;
+}
+
+#permissionsDisableDescription {
+ color: var(--text-color-deemphasized);
+ line-height: 110%;
+}
+
+#permissionsDisableCheckbox {
+ margin-inline-start: 4px;
+ padding-top: 10px;
+}
diff --git a/browser/components/preferences/dialogs/sitePermissions.js b/browser/components/preferences/dialogs/sitePermissions.js
new file mode 100644
index 0000000000..61af43463b
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.js
@@ -0,0 +1,679 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../extensionControlled.js */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { SitePermissions } = ChromeUtils.importESModule(
+ "resource:///modules/SitePermissions.sys.mjs"
+);
+
+const sitePermissionsL10n = {
+ "desktop-notification": {
+ window: "permissions-site-notification-window2",
+ description: "permissions-site-notification-desc",
+ disableLabel: "permissions-site-notification-disable-label",
+ disableDescription: "permissions-site-notification-disable-desc",
+ },
+ geo: {
+ window: "permissions-site-location-window2",
+ description: "permissions-site-location-desc",
+ disableLabel: "permissions-site-location-disable-label",
+ disableDescription: "permissions-site-location-disable-desc",
+ },
+ xr: {
+ window: "permissions-site-xr-window2",
+ description: "permissions-site-xr-desc",
+ disableLabel: "permissions-site-xr-disable-label",
+ disableDescription: "permissions-site-xr-disable-desc",
+ },
+ camera: {
+ window: "permissions-site-camera-window2",
+ description: "permissions-site-camera-desc",
+ disableLabel: "permissions-site-camera-disable-label",
+ disableDescription: "permissions-site-camera-disable-desc",
+ },
+ microphone: {
+ window: "permissions-site-microphone-window2",
+ description: "permissions-site-microphone-desc",
+ disableLabel: "permissions-site-microphone-disable-label",
+ disableDescription: "permissions-site-microphone-disable-desc",
+ },
+ speaker: {
+ window: "permissions-site-speaker-window",
+ description: "permissions-site-speaker-desc",
+ },
+ "autoplay-media": {
+ window: "permissions-site-autoplay-window2",
+ description: "permissions-site-autoplay-desc",
+ },
+};
+
+const sitePermissionsConfig = {
+ "autoplay-media": {
+ _getCapabilityString(capability) {
+ switch (capability) {
+ case SitePermissions.ALLOW:
+ return "permissions-capabilities-autoplay-allow";
+ case SitePermissions.BLOCK:
+ return "permissions-capabilities-autoplay-block";
+ case SitePermissions.AUTOPLAY_BLOCKED_ALL:
+ return "permissions-capabilities-autoplay-blockall";
+ }
+ throw new Error(`Unknown capability: ${capability}`);
+ },
+ },
+};
+
+// A set of permissions for a single origin. One PermissionGroup instance
+// corresponds to one row in the gSitePermissionsManager._list richlistbox.
+// Permissions may be single or double keyed, but the primary key of all
+// permissions matches the permission type of the dialog.
+class PermissionGroup {
+ #changedCapability;
+
+ constructor(perm) {
+ this.principal = perm.principal;
+ this.origin = perm.principal.origin;
+ this.perms = [perm];
+ }
+ addPermission(perm) {
+ this.perms.push(perm);
+ }
+ removePermission(perm) {
+ this.perms = this.perms.filter(p => p.type != perm.type);
+ }
+ set capability(cap) {
+ this.#changedCapability = cap;
+ }
+ get capability() {
+ if (this.#changedCapability) {
+ return this.#changedCapability;
+ }
+ return this.savedCapability;
+ }
+ revert() {
+ this.#changedCapability = null;
+ }
+ get savedCapability() {
+ // This logic to present a single capability for permissions of different
+ // keys and capabilities caters for speaker-selection, where a block
+ // permission may be set for all devices with no second key, which would
+ // override any device-specific double-keyed allow permissions.
+ let cap;
+ for (let perm of this.perms) {
+ let [type] = perm.type.split(SitePermissions.PERM_KEY_DELIMITER);
+ if (type == perm.type) {
+ // No second key. This overrides double-keyed perms.
+ return perm.capability;
+ }
+ // Double-keyed perms are not expected to have different capabilities.
+ cap = perm.capability;
+ }
+ return cap;
+ }
+}
+
+const PERMISSION_STATES = [
+ SitePermissions.ALLOW,
+ SitePermissions.BLOCK,
+ SitePermissions.PROMPT,
+ SitePermissions.AUTOPLAY_BLOCKED_ALL,
+];
+
+const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled";
+const NOTIFICATIONS_PERMISSION_PREF =
+ "permissions.default.desktop-notification";
+
+const AUTOPLAY_PREF = "media.autoplay.default";
+
+var gSitePermissionsManager = {
+ _type: "",
+ _isObserving: false,
+ _permissionGroups: new Map(),
+ _permissionsToChange: new Map(),
+ _permissionsToDelete: new Map(),
+ _list: null,
+ _removeButton: null,
+ _removeAllButton: null,
+ _searchBox: null,
+ _checkbox: null,
+ _currentDefaultPermissionsState: null,
+ _defaultPermissionStatePrefName: null,
+
+ onLoad() {
+ let params = window.arguments[0];
+ document.mozSubdialogReady = this.init(params);
+ },
+
+ async init(params) {
+ if (!this._isObserving) {
+ Services.obs.addObserver(this, "perm-changed");
+ this._isObserving = true;
+ }
+
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ this._type = params.permissionType;
+ this._list = document.getElementById("permissionsBox");
+ this._removeButton = document.getElementById("removePermission");
+ this._removeAllButton = document.getElementById("removeAllPermissions");
+ this._searchBox = document.getElementById("searchBox");
+ this._checkbox = document.getElementById("permissionsDisableCheckbox");
+ this._disableExtensionButton = document.getElementById(
+ "disableNotificationsPermissionExtension"
+ );
+ this._permissionsDisableDescription = document.getElementById(
+ "permissionsDisableDescription"
+ );
+ this._setAutoplayPref = document.getElementById("setAutoplayPref");
+
+ let permissionsText = document.getElementById("permissionsText");
+
+ document.l10n.pauseObserving();
+ let l10n = sitePermissionsL10n[this._type];
+ document.l10n.setAttributes(permissionsText, l10n.description);
+ if (l10n.disableLabel) {
+ document.l10n.setAttributes(this._checkbox, l10n.disableLabel);
+ }
+ if (l10n.disableDescription) {
+ document.l10n.setAttributes(
+ this._permissionsDisableDescription,
+ l10n.disableDescription
+ );
+ }
+ document.l10n.setAttributes(document.documentElement, l10n.window);
+
+ await document.l10n.translateElements([
+ permissionsText,
+ this._checkbox,
+ this._permissionsDisableDescription,
+ document.documentElement,
+ ]);
+ document.l10n.resumeObserving();
+
+ // Initialize the checkbox state and handle showing notification permission UI
+ // when it is disabled by an extension.
+ this._defaultPermissionStatePrefName = "permissions.default." + this._type;
+ this._watchPermissionPrefChange();
+
+ this._loadPermissions();
+ this.buildPermissionsList();
+
+ if (params.permissionType == "autoplay-media") {
+ await this.buildAutoplayMenulist();
+ this._setAutoplayPref.hidden = false;
+ }
+
+ this._searchBox.focus();
+ },
+
+ uninit() {
+ if (this._isObserving) {
+ Services.obs.removeObserver(this, "perm-changed");
+ this._isObserving = false;
+ }
+ if (this._setAutoplayPref) {
+ this._setAutoplayPref.hidden = true;
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic !== "perm-changed") {
+ return;
+ }
+
+ let permission = subject.QueryInterface(Ci.nsIPermission);
+ let [type] = permission.type.split(SitePermissions.PERM_KEY_DELIMITER);
+
+ // Ignore unrelated permission types and permissions with unknown states.
+ if (
+ type !== this._type ||
+ !PERMISSION_STATES.includes(permission.capability)
+ ) {
+ return;
+ }
+
+ if (data == "added") {
+ this._addPermissionToList(permission);
+ } else {
+ let group = this._permissionGroups.get(permission.principal.origin);
+ if (!group) {
+ // already moved to _permissionsToDelete
+ // or private browsing session permission
+ return;
+ }
+ if (data == "changed") {
+ group.removePermission(permission);
+ group.addPermission(permission);
+ } else if (data == "deleted") {
+ group.removePermission(permission);
+ if (!group.perms.length) {
+ this._removePermissionFromList(permission.principal.origin);
+ return;
+ }
+ }
+ }
+ this.buildPermissionsList();
+ },
+
+ _handleCheckboxUIUpdates() {
+ let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName);
+ if (pref != Services.prefs.PREF_INVALID) {
+ this._currentDefaultPermissionsState = Services.prefs.getIntPref(
+ this._defaultPermissionStatePrefName
+ );
+ }
+
+ if (this._currentDefaultPermissionsState === null) {
+ this._checkbox.hidden = true;
+ this._permissionsDisableDescription.hidden = true;
+ } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
+ this._checkbox.checked = true;
+ } else {
+ this._checkbox.checked = false;
+ }
+
+ if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) {
+ this._checkbox.disabled = true;
+ }
+ },
+
+ /**
+ * Listen for changes to the permissions.default.* pref and make
+ * necessary changes to the UI.
+ */
+ _watchPermissionPrefChange() {
+ this._handleCheckboxUIUpdates();
+
+ if (this._type == "desktop-notification") {
+ this._handleWebNotificationsDisable();
+
+ this._disableExtensionButton.addEventListener(
+ "command",
+ makeDisableControllingExtension(
+ PREF_SETTING_TYPE,
+ NOTIFICATIONS_PERMISSION_OVERRIDE_KEY
+ )
+ );
+ }
+
+ let observer = () => {
+ this._handleCheckboxUIUpdates();
+ if (this._type == "desktop-notification") {
+ this._handleWebNotificationsDisable();
+ }
+ };
+ Services.prefs.addObserver(this._defaultPermissionStatePrefName, observer);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver(
+ this._defaultPermissionStatePrefName,
+ observer
+ );
+ });
+ },
+
+ /**
+ * Handles the UI update for web notifications disable by extensions.
+ */
+ async _handleWebNotificationsDisable() {
+ let prefLocked = Services.prefs.prefIsLocked(NOTIFICATIONS_PERMISSION_PREF);
+ if (prefLocked) {
+ // An extension can't control these settings if they're locked.
+ hideControllingExtension(NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
+ } else {
+ let isControlled = await handleControllingExtension(
+ PREF_SETTING_TYPE,
+ NOTIFICATIONS_PERMISSION_OVERRIDE_KEY
+ );
+ this._checkbox.disabled = isControlled;
+ }
+ },
+
+ _getCapabilityL10nId(element, type, capability) {
+ if (
+ type in sitePermissionsConfig &&
+ sitePermissionsConfig[type]._getCapabilityString
+ ) {
+ return sitePermissionsConfig[type]._getCapabilityString(capability);
+ }
+ switch (element.tagName) {
+ case "menuitem":
+ switch (capability) {
+ case Services.perms.ALLOW_ACTION:
+ return "permissions-capabilities-allow";
+ case Services.perms.DENY_ACTION:
+ return "permissions-capabilities-block";
+ case Services.perms.PROMPT_ACTION:
+ return "permissions-capabilities-prompt";
+ default:
+ throw new Error(`Unknown capability: ${capability}`);
+ }
+ case "label":
+ switch (capability) {
+ case Services.perms.ALLOW_ACTION:
+ return "permissions-capabilities-listitem-allow";
+ case Services.perms.DENY_ACTION:
+ return "permissions-capabilities-listitem-block";
+ default:
+ throw new Error(`Unexpected capability: ${capability}`);
+ }
+ default:
+ throw new Error(`Unexpected tag: ${element.tagName}`);
+ }
+ },
+
+ _addPermissionToList(perm) {
+ let [type] = perm.type.split(SitePermissions.PERM_KEY_DELIMITER);
+ // Ignore unrelated permission types and permissions with unknown states.
+ if (
+ type !== this._type ||
+ !PERMISSION_STATES.includes(perm.capability) ||
+ // Skip private browsing session permissions
+ (perm.principal.privateBrowsingId !==
+ Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID &&
+ perm.expireType === Services.perms.EXPIRE_SESSION)
+ ) {
+ return;
+ }
+ let group = this._permissionGroups.get(perm.principal.origin);
+ if (group) {
+ group.addPermission(perm);
+ } else {
+ group = new PermissionGroup(perm);
+ this._permissionGroups.set(group.origin, group);
+ }
+ },
+
+ _removePermissionFromList(origin) {
+ this._permissionGroups.delete(origin);
+ this._permissionsToChange.delete(origin);
+ let permissionlistitem = document.getElementsByAttribute(
+ "origin",
+ origin
+ )[0];
+ if (permissionlistitem) {
+ permissionlistitem.remove();
+ }
+ },
+
+ _loadPermissions() {
+ // load permissions into a table.
+ for (let nextPermission of Services.perms.all) {
+ this._addPermissionToList(nextPermission);
+ }
+ },
+
+ _createPermissionListItem(permissionGroup) {
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("origin", permissionGroup.origin);
+ let row = document.createXULElement("hbox");
+
+ let hbox = document.createXULElement("hbox");
+ let website = document.createXULElement("label");
+ website.setAttribute("value", permissionGroup.origin);
+ hbox.setAttribute("class", "website-name");
+ hbox.appendChild(website);
+
+ let states = SitePermissions.getAvailableStates(this._type).filter(
+ state => state != SitePermissions.UNKNOWN
+ );
+ // Handle the cases of a double-keyed ALLOW permission or a PROMPT
+ // permission after the default has been changed back to UNKNOWN.
+ if (!states.includes(permissionGroup.savedCapability)) {
+ states.unshift(permissionGroup.savedCapability);
+ }
+ let siteStatus;
+ if (states.length == 1) {
+ // Only a single state is available. Show a label.
+ siteStatus = document.createXULElement("hbox");
+ let label = document.createXULElement("label");
+ siteStatus.appendChild(label);
+ document.l10n.setAttributes(
+ label,
+ this._getCapabilityL10nId(label, this._type, permissionGroup.capability)
+ );
+ } else {
+ // Multiple states are available. Show a menulist.
+ siteStatus = document.createXULElement("menulist");
+ for (let state of states) {
+ let m = siteStatus.appendItem(undefined, state);
+ document.l10n.setAttributes(
+ m,
+ this._getCapabilityL10nId(m, this._type, state)
+ );
+ }
+ siteStatus.addEventListener("select", () => {
+ this.onPermissionChange(permissionGroup, Number(siteStatus.value));
+ });
+ }
+ siteStatus.setAttribute("class", "website-status");
+ siteStatus.value = permissionGroup.capability;
+
+ row.appendChild(hbox);
+ row.appendChild(siteStatus);
+ richlistitem.appendChild(row);
+ return richlistitem;
+ },
+
+ onPermissionKeyPress(event) {
+ if (!this._list.selectedItem) {
+ return;
+ }
+
+ if (
+ event.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ this.onPermissionDelete();
+ event.preventDefault();
+ }
+ },
+
+ _setRemoveButtonState() {
+ if (!this._list) {
+ return;
+ }
+
+ let hasSelection = this._list.selectedIndex >= 0;
+ let hasRows = this._list.itemCount > 0;
+ this._removeButton.disabled = !hasSelection;
+ this._removeAllButton.disabled = !hasRows;
+ },
+
+ onPermissionDelete() {
+ let richlistitem = this._list.selectedItem;
+ let origin = richlistitem.getAttribute("origin");
+ let permissionGroup = this._permissionGroups.get(origin);
+
+ this._removePermissionFromList(origin);
+ this._permissionsToDelete.set(permissionGroup.origin, permissionGroup);
+
+ this._setRemoveButtonState();
+ },
+
+ onAllPermissionsDelete() {
+ for (let permissionGroup of this._permissionGroups.values()) {
+ this._removePermissionFromList(permissionGroup.origin);
+ this._permissionsToDelete.set(permissionGroup.origin, permissionGroup);
+ }
+
+ this._setRemoveButtonState();
+ },
+
+ onPermissionSelect() {
+ this._setRemoveButtonState();
+ },
+
+ onPermissionChange(perm, capability) {
+ let group = this._permissionGroups.get(perm.origin);
+ if (group.capability == capability) {
+ return;
+ }
+ if (capability == group.savedCapability) {
+ group.revert();
+ this._permissionsToChange.delete(group.origin);
+ } else {
+ group.capability = capability;
+ this._permissionsToChange.set(group.origin, group);
+ }
+
+ // enable "remove all" button as needed
+ this._setRemoveButtonState();
+ },
+
+ onApplyChanges() {
+ // Stop observing permission changes since we are about
+ // to write out the pending adds/deletes and don't need
+ // to update the UI
+ this.uninit();
+
+ // Delete even _permissionsToChange to clear out double-keyed permissions
+ for (let group of [
+ ...this._permissionsToDelete.values(),
+ ...this._permissionsToChange.values(),
+ ]) {
+ for (let perm of group.perms) {
+ SitePermissions.removeFromPrincipal(perm.principal, perm.type);
+ }
+ }
+
+ for (let group of this._permissionsToChange.values()) {
+ SitePermissions.setForPrincipal(
+ group.principal,
+ this._type,
+ group.capability
+ );
+ }
+
+ if (this._checkbox.checked) {
+ Services.prefs.setIntPref(
+ this._defaultPermissionStatePrefName,
+ SitePermissions.BLOCK
+ );
+ } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
+ Services.prefs.setIntPref(
+ this._defaultPermissionStatePrefName,
+ SitePermissions.UNKNOWN
+ );
+ }
+ },
+
+ buildPermissionsList(sortCol) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+ let frag = document.createDocumentFragment();
+
+ let permissionGroups = Array.from(this._permissionGroups.values());
+
+ let keyword = this._searchBox.value.toLowerCase().trim();
+ for (let permissionGroup of permissionGroups) {
+ if (keyword && !permissionGroup.origin.includes(keyword)) {
+ continue;
+ }
+
+ let richlistitem = this._createPermissionListItem(permissionGroup);
+ frag.appendChild(richlistitem);
+ }
+
+ // Sort permissions.
+ this._sortPermissions(this._list, frag, sortCol);
+
+ this._list.appendChild(frag);
+
+ this._setRemoveButtonState();
+ },
+
+ async buildAutoplayMenulist() {
+ let menulist = document.createXULElement("menulist");
+ let states = SitePermissions.getAvailableStates("autoplay-media");
+ document.l10n.pauseObserving();
+ for (let state of states) {
+ let m = menulist.appendItem(undefined, state);
+ document.l10n.setAttributes(
+ m,
+ this._getCapabilityL10nId(m, "autoplay-media", state)
+ );
+ }
+
+ menulist.value = SitePermissions.getDefault("autoplay-media");
+
+ menulist.addEventListener("select", () => {
+ SitePermissions.setDefault("autoplay-media", Number(menulist.value));
+ });
+
+ menulist.menupopup.setAttribute("incontentshell", "false");
+
+ menulist.disabled = Services.prefs.prefIsLocked(AUTOPLAY_PREF);
+
+ document.getElementById("setAutoplayPref").appendChild(menulist);
+ await document.l10n.translateFragment(menulist);
+ document.l10n.resumeObserving();
+ },
+
+ _sortPermissions(list, frag, column) {
+ let sortDirection;
+
+ if (!column) {
+ column = document.querySelector("treecol[data-isCurrentSortCol=true]");
+ sortDirection =
+ column.getAttribute("data-last-sortDirection") || "ascending";
+ } else {
+ sortDirection = column.getAttribute("data-last-sortDirection");
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = null;
+ switch (column.id) {
+ case "siteCol":
+ sortFunc = (a, b) => {
+ return comp.compare(
+ a.getAttribute("origin"),
+ b.getAttribute("origin")
+ );
+ };
+ break;
+
+ case "statusCol":
+ sortFunc = (a, b) => {
+ return (
+ parseInt(a.querySelector(".website-status").value) >
+ parseInt(b.querySelector(".website-status").value)
+ );
+ };
+ break;
+ }
+
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.querySelectorAll("richlistitem"));
+
+ if (sortDirection === "descending") {
+ items.sort((a, b) => sortFunc(b, a));
+ } else {
+ items.sort(sortFunc);
+ }
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ let cols = list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("data-isCurrentSortCol");
+ c.removeAttribute("sortDirection");
+ });
+ column.setAttribute("data-isCurrentSortCol", "true");
+ column.setAttribute("sortDirection", sortDirection);
+ column.setAttribute("data-last-sortDirection", sortDirection);
+ },
+};
diff --git a/browser/components/preferences/dialogs/sitePermissions.xhtml b/browser/components/preferences/dialogs/sitePermissions.xhtml
new file mode 100644
index 0000000000..5cc307aced
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.xhtml
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="SitePermissionsDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="permissions-window2"
+ data-l10n-attrs="title, style"
+ onload="gSitePermissionsManager.onLoad();"
+ onunload="gSitePermissionsManager.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="permission-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/preferences.ftl"
+ />
+ <html:link
+ rel="localization"
+ href="browser/preferences/permissions.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/sitePermissions.js" />
+ <script src="chrome://browser/content/preferences/extensionControlled.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="permissions-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <hbox align="center" id="setAutoplayPref" hidden="true">
+ <label data-l10n-id="permissions-autoplay-menu" />
+ </hbox>
+ <description id="permissionsText" control="url" />
+ <separator class="thin" />
+ <hbox align="start">
+ <search-textbox
+ id="searchBox"
+ flex="1"
+ data-l10n-id="permissions-searchbox"
+ data-l10n-attrs="placeholder"
+ oncommand="gSitePermissionsManager.buildPermissionsList();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <listheader>
+ <treecol
+ id="siteCol"
+ data-l10n-id="permissions-site-name"
+ onclick="gSitePermissionsManager.buildPermissionsList(event.target)"
+ />
+ <treecol
+ id="statusCol"
+ data-l10n-id="permissions-status"
+ data-isCurrentSortCol="true"
+ onclick="gSitePermissionsManager.buildPermissionsList(event.target);"
+ />
+ </listheader>
+ <richlistbox
+ id="permissionsBox"
+ selected="false"
+ onkeypress="gSitePermissionsManager.onPermissionKeyPress(event);"
+ onselect="gSitePermissionsManager.onPermissionSelect();"
+ />
+ </vbox>
+
+ <hbox class="actionButtons">
+ <button
+ id="removePermission"
+ disabled="true"
+ data-l10n-id="permissions-remove"
+ oncommand="gSitePermissionsManager.onPermissionDelete();"
+ />
+ <button
+ id="removeAllPermissions"
+ data-l10n-id="permissions-remove-all"
+ oncommand="gSitePermissionsManager.onAllPermissionsDelete();"
+ />
+ </hbox>
+
+ <checkbox id="permissionsDisableCheckbox" />
+ <description id="permissionsDisableDescription" />
+ <hbox
+ id="browserNotificationsPermissionExtensionContent"
+ class="extension-controlled"
+ align="center"
+ hidden="true"
+ >
+ <description control="disableNotificationsPermissionExtension" flex="1" />
+ <button
+ id="disableNotificationsPermissionExtension"
+ class="extension-controlled-button accessory-button"
+ data-l10n-id="disable-extension"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/syncChooseWhatToSync.js b/browser/components/preferences/dialogs/syncChooseWhatToSync.js
new file mode 100644
index 0000000000..2cc965b4e1
--- /dev/null
+++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "services.sync.engine.addons", type: "bool" },
+ { id: "services.sync.engine.bookmarks", type: "bool" },
+ { id: "services.sync.engine.history", type: "bool" },
+ { id: "services.sync.engine.tabs", type: "bool" },
+ { id: "services.sync.engine.prefs", type: "bool" },
+ { id: "services.sync.engine.passwords", type: "bool" },
+ { id: "services.sync.engine.addresses", type: "bool" },
+ { id: "services.sync.engine.creditcards", type: "bool" },
+]);
+
+let gSyncChooseWhatToSync = {
+ init() {
+ this._adjustForPrefs();
+ let options = window.arguments[0];
+ if (options.disconnectFun) {
+ // We offer 'disconnect'
+ document.addEventListener("dialogextra2", function () {
+ options.disconnectFun().then(disconnected => {
+ if (disconnected) {
+ window.close();
+ }
+ });
+ });
+ } else {
+ // no "disconnect" - hide the button.
+ document
+ .getElementById("syncChooseOptions")
+ .getButton("extra2").hidden = true;
+ }
+ },
+
+ // make whatever tweaks we need based on preferences.
+ _adjustForPrefs() {
+ // These 2 engines are unique in that there are prefs that make the
+ // entire engine unavailable (which is distinct from "disabled").
+ let enginePrefs = [
+ ["services.sync.engine.addresses", ".sync-engine-addresses"],
+ ["services.sync.engine.creditcards", ".sync-engine-creditcards"],
+ ];
+ for (let [enabledPref, className] of enginePrefs) {
+ let availablePref = enabledPref + ".available";
+ // If the engine is enabled we force it to be available, otherwise we see
+ // spooky things happen (like it magically re-appear later)
+ if (Services.prefs.getBoolPref(enabledPref, false)) {
+ Services.prefs.setBoolPref(availablePref, true);
+ }
+ if (!Services.prefs.getBoolPref(availablePref)) {
+ let elt = document.querySelector(className);
+ elt.hidden = true;
+ }
+ }
+ },
+};
diff --git a/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
new file mode 100644
index 0000000000..0bed6913d4
--- /dev/null
+++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gSyncChooseWhatToSync.init();"
+ data-l10n-id="sync-choose-what-to-sync-dialog3"
+ data-l10n-attrs="title, style"
+>
+ <dialog
+ id="syncChooseOptions"
+ buttons="accept,cancel,extra2"
+ data-l10n-id="sync-choose-what-to-sync-dialog3"
+ data-l10n-attrs="buttonlabelaccept, buttonlabelextra2"
+ >
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/preferences.ftl"
+ />
+ <html:link rel="localization" href="toolkit/branding/accounts.ftl" />
+ </linkset>
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js" />
+ <description
+ class="sync-choose-dialog-description"
+ data-l10n-id="sync-choose-dialog-subtitle"
+ />
+ <html:div class="sync-engines-list">
+ <html:div class="sync-engine-bookmarks">
+ <checkbox
+ data-l10n-id="sync-engine-bookmarks"
+ preference="services.sync.engine.bookmarks"
+ />
+ </html:div>
+ <html:div class="sync-engine-history">
+ <checkbox
+ data-l10n-id="sync-engine-history"
+ preference="services.sync.engine.history"
+ />
+ </html:div>
+ <html:div class="sync-engine-tabs">
+ <checkbox
+ data-l10n-id="sync-engine-tabs"
+ preference="services.sync.engine.tabs"
+ />
+ </html:div>
+ <html:div class="sync-engine-passwords">
+ <checkbox
+ data-l10n-id="sync-engine-logins-passwords"
+ preference="services.sync.engine.passwords"
+ />
+ </html:div>
+ <html:div class="sync-engine-addresses">
+ <checkbox
+ data-l10n-id="sync-engine-addresses"
+ preference="services.sync.engine.addresses"
+ />
+ </html:div>
+ <html:div class="sync-engine-creditcards">
+ <checkbox
+ data-l10n-id="sync-engine-creditcards"
+ preference="services.sync.engine.creditcards"
+ />
+ </html:div>
+ <html:div class="sync-engine-addons">
+ <checkbox
+ data-l10n-id="sync-engine-addons"
+ preference="services.sync.engine.addons"
+ />
+ </html:div>
+ <html:div class="sync-engine-prefs">
+ <checkbox
+ data-l10n-id="sync-engine-settings"
+ preference="services.sync.engine.prefs"
+ />
+ </html:div>
+ </html:div>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/translationExceptions.js b/browser/components/preferences/dialogs/translationExceptions.js
new file mode 100644
index 0000000000..27579594c9
--- /dev/null
+++ b/browser/components/preferences/dialogs/translationExceptions.js
@@ -0,0 +1,256 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// TODO (Bug 1817084) Remove this file when we disable the extension
+
+"use strict";
+
+const kPermissionType = "translate";
+const kLanguagesPref = "browser.translation.neverForLanguages";
+
+function Tree(aId, aData) {
+ this._data = aData;
+ this._tree = document.getElementById(aId);
+ this._tree.view = this;
+}
+
+Tree.prototype = {
+ get tree() {
+ return this._tree;
+ },
+ get isEmpty() {
+ return !this._data.length;
+ },
+ get hasSelection() {
+ return this.selection.count > 0;
+ },
+ getSelectedItems() {
+ let result = [];
+
+ let rc = this.selection.getRangeCount();
+ for (let i = 0; i < rc; ++i) {
+ let min = {},
+ max = {};
+ this.selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ result.push(this._data[j]);
+ }
+ }
+
+ return result;
+ },
+
+ // nsITreeView implementation
+ get rowCount() {
+ return this._data.length;
+ },
+ getCellText(aRow, aColumn) {
+ return this._data[aRow];
+ },
+ isSeparator(aIndex) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(aIndex) {
+ return false;
+ },
+ setTree(aTree) {},
+ getImageSrc(aRow, aColumn) {},
+ getCellValue(aRow, aColumn) {},
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ return "";
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+};
+
+function Lang(aCode, label) {
+ this.langCode = aCode;
+ this._label = label;
+}
+
+Lang.prototype = {
+ toString() {
+ return this._label;
+ },
+};
+
+var gTranslationExceptions = {
+ onLoad() {
+ if (this._siteTree) {
+ // Re-using an open dialog, clear the old observers.
+ this.uninit();
+ }
+
+ // Load site permissions into an array.
+ this._sites = [];
+ for (let perm of Services.perms.all) {
+ if (
+ perm.type == kPermissionType &&
+ perm.capability == Services.perms.DENY_ACTION
+ ) {
+ this._sites.push(perm.principal.origin);
+ }
+ }
+ Services.obs.addObserver(this, "perm-changed");
+ this._sites.sort();
+
+ this._siteTree = new Tree("sitesTree", this._sites);
+ this.onSiteSelected();
+
+ this._langs = this.getLanguageExceptions();
+ Services.prefs.addObserver(kLanguagesPref, this);
+ this._langTree = new Tree("languagesTree", this._langs);
+ this.onLanguageSelected();
+ },
+
+ // Get the list of languages we don't translate as an array.
+ getLanguageExceptions() {
+ let langs = Services.prefs.getCharPref(kLanguagesPref);
+ if (!langs) {
+ return [];
+ }
+
+ let langArr = langs.split(",");
+ let displayNames = Services.intl.getLanguageDisplayNames(
+ undefined,
+ langArr
+ );
+ let result = langArr.map((lang, i) => new Lang(lang, displayNames[i]));
+ result.sort();
+
+ return result;
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "perm-changed") {
+ if (aData == "cleared") {
+ if (!this._sites.length) {
+ return;
+ }
+ let removed = this._sites.splice(0, this._sites.length);
+ this._siteTree.tree.rowCountChanged(0, -removed.length);
+ } else {
+ let perm = aSubject.QueryInterface(Ci.nsIPermission);
+ if (perm.type != kPermissionType) {
+ return;
+ }
+
+ if (aData == "added") {
+ if (perm.capability != Services.perms.DENY_ACTION) {
+ return;
+ }
+ this._sites.push(perm.principal.origin);
+ this._sites.sort();
+ let tree = this._siteTree.tree;
+ tree.rowCountChanged(0, 1);
+ tree.invalidate();
+ } else if (aData == "deleted") {
+ let index = this._sites.indexOf(perm.principal.origin);
+ if (index == -1) {
+ return;
+ }
+ this._sites.splice(index, 1);
+ this._siteTree.tree.rowCountChanged(index, -1);
+ this.onSiteSelected();
+ return;
+ }
+ }
+ this.onSiteSelected();
+ } else if (aTopic == "nsPref:changed") {
+ this._langs = this.getLanguageExceptions();
+ let change = this._langs.length - this._langTree.rowCount;
+ this._langTree._data = this._langs;
+ let tree = this._langTree.tree;
+ if (change) {
+ tree.rowCountChanged(0, change);
+ }
+ tree.invalidate();
+ this.onLanguageSelected();
+ }
+ },
+
+ _handleButtonDisabling(aTree, aIdPart) {
+ let empty = aTree.isEmpty;
+ document.getElementById("removeAll" + aIdPart + "s").disabled = empty;
+ document.getElementById("remove" + aIdPart).disabled =
+ empty || !aTree.hasSelection;
+ },
+
+ onLanguageSelected() {
+ this._handleButtonDisabling(this._langTree, "Language");
+ },
+
+ onSiteSelected() {
+ this._handleButtonDisabling(this._siteTree, "Site");
+ },
+
+ onLanguageDeleted() {
+ let langs = Services.prefs.getCharPref(kLanguagesPref);
+ if (!langs) {
+ return;
+ }
+
+ let removed = this._langTree.getSelectedItems().map(l => l.langCode);
+
+ langs = langs.split(",").filter(l => !removed.includes(l));
+ Services.prefs.setCharPref(kLanguagesPref, langs.join(","));
+ },
+
+ onAllLanguagesDeleted() {
+ Services.prefs.setCharPref(kLanguagesPref, "");
+ },
+
+ onSiteDeleted() {
+ let removedSites = this._siteTree.getSelectedItems();
+ for (let origin of removedSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, kPermissionType);
+ }
+ },
+
+ onAllSitesDeleted() {
+ if (this._siteTree.isEmpty) {
+ return;
+ }
+
+ let removedSites = this._sites.splice(0, this._sites.length);
+ this._siteTree.tree.rowCountChanged(0, -removedSites.length);
+
+ for (let origin of removedSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, kPermissionType);
+ }
+
+ this.onSiteSelected();
+ },
+
+ onSiteKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onSiteDeleted();
+ }
+ },
+
+ onLanguageKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onLanguageDeleted();
+ }
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "perm-changed");
+ Services.prefs.removeObserver(kLanguagesPref, this);
+ },
+};
diff --git a/browser/components/preferences/dialogs/translationExceptions.xhtml b/browser/components/preferences/dialogs/translationExceptions.xhtml
new file mode 100644
index 0000000000..b824ce86a5
--- /dev/null
+++ b/browser/components/preferences/dialogs/translationExceptions.xhtml
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- TODO (Bug 1817084) Remove this file when we disable the extension -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="TranslationDialog"
+ data-l10n-id="translation-window2"
+ data-l10n-attrs="title, style"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gTranslationExceptions.onLoad();"
+ onunload="gTranslationExceptions.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept"
+ data-l10n-id="translation-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/translation.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/translationExceptions.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="translation-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <vbox flex="1">
+ <label
+ id="languagesLabel"
+ data-l10n-id="translation-languages-disabled-desc"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="languagesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationExceptions.onLanguageKeyPress(event)"
+ onselect="gTranslationExceptions.onLanguageSelected();"
+ >
+ <treecols>
+ <treecol
+ id="languageCol"
+ data-l10n-id="translation-languages-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="end">
+ <button
+ id="removeLanguage"
+ disabled="true"
+ data-l10n-id="translation-languages-button-remove"
+ oncommand="gTranslationExceptions.onLanguageDeleted();"
+ />
+ <button
+ id="removeAllLanguages"
+ data-l10n-id="translation-languages-button-remove-all"
+ oncommand="gTranslationExceptions.onAllLanguagesDeleted();"
+ />
+ <spacer flex="1" />
+ </hbox>
+ <separator />
+ <vbox flex="1">
+ <label
+ id="languagesLabel"
+ data-l10n-id="translation-sites-disabled-desc"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="sitesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationExceptions.onSiteKeyPress(event)"
+ onselect="gTranslationExceptions.onSiteSelected();"
+ >
+ <treecols>
+ <treecol
+ id="siteCol"
+ data-l10n-id="translation-sites-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ </vbox>
+
+ <hbox class="actionButtons" pack="end">
+ <button
+ id="removeSite"
+ disabled="true"
+ data-l10n-id="translation-sites-button-remove"
+ oncommand="gTranslationExceptions.onSiteDeleted();"
+ />
+ <button
+ id="removeAllSites"
+ data-l10n-id="translation-sites-button-remove-all"
+ oncommand="gTranslationExceptions.onAllSitesDeleted();"
+ />
+ <spacer flex="1" />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/translations.js b/browser/components/preferences/dialogs/translations.js
new file mode 100644
index 0000000000..30d2b22f17
--- /dev/null
+++ b/browser/components/preferences/dialogs/translations.js
@@ -0,0 +1,465 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * The permission type to give to Services.perms for Translations.
+ */
+const TRANSLATIONS_PERMISSION = "translations";
+/**
+ * The list of BCP-47 language tags that will trigger auto-translate.
+ */
+const ALWAYS_TRANSLATE_LANGS_PREF =
+ "browser.translations.alwaysTranslateLanguages";
+/**
+ * The list of BCP-47 language tags that will prevent showing Translations UI.
+ */
+const NEVER_TRANSLATE_LANGS_PREF =
+ "browser.translations.neverTranslateLanguages";
+
+function Tree(aId, aData) {
+ this._data = aData;
+ this._tree = document.getElementById(aId);
+ this._tree.view = this;
+}
+
+Tree.prototype = {
+ get tree() {
+ return this._tree;
+ },
+ get isEmpty() {
+ return !this._data.length;
+ },
+ get hasSelection() {
+ return this.selection.count > 0;
+ },
+ getSelectedItems() {
+ let result = [];
+
+ let rc = this.selection.getRangeCount();
+ for (let i = 0; i < rc; ++i) {
+ let min = {},
+ max = {};
+ this.selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ result.push(this._data[j]);
+ }
+ }
+
+ return result;
+ },
+
+ // nsITreeView implementation
+ get rowCount() {
+ return this._data.length;
+ },
+ getCellText(aRow, aColumn) {
+ return this._data[aRow];
+ },
+ isSeparator(aIndex) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(aIndex) {
+ return false;
+ },
+ setTree(aTree) {},
+ getImageSrc(aRow, aColumn) {},
+ getCellValue(aRow, aColumn) {},
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ return "";
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+};
+
+function Lang(aCode, label) {
+ this.langCode = aCode;
+ this._label = label;
+}
+
+Lang.prototype = {
+ toString() {
+ return this._label;
+ },
+};
+
+var gTranslationsSettings = {
+ onLoad() {
+ if (this._neverTranslateSiteTree) {
+ // Re-using an open dialog, clear the old observers.
+ this.removeObservers();
+ }
+
+ // Load site permissions into an array.
+ this._neverTranslateSites = [];
+ for (const perm of Services.perms.getAllByTypes([
+ TRANSLATIONS_PERMISSION,
+ ])) {
+ if (perm.capability === Services.perms.DENY_ACTION) {
+ this._neverTranslateSites.push(perm.principal.origin);
+ }
+ }
+ let stripProtocol = s => s?.replace(/^\w+:/, "") || "";
+ this._neverTranslateSites.sort((a, b) => {
+ return stripProtocol(a).localeCompare(stripProtocol(b));
+ });
+
+ // Load language tags into arrays.
+ this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();
+ this._neverTranslateLangs = this.getNeverTranslateLanguages();
+
+ // Add observers for relevant prefs and permissions.
+ Services.obs.addObserver(this, "perm-changed");
+ Services.prefs.addObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
+ Services.prefs.addObserver(NEVER_TRANSLATE_LANGS_PREF, this);
+
+ // Build trees from the arrays.
+ this._alwaysTranslateLangsTree = new Tree(
+ "alwaysTranslateLanguagesTree",
+ this._alwaysTranslateLangs
+ );
+ this._neverTranslateLangsTree = new Tree(
+ "neverTranslateLanguagesTree",
+ this._neverTranslateLangs
+ );
+ this._neverTranslateSiteTree = new Tree(
+ "neverTranslateSitesTree",
+ this._neverTranslateSites
+ );
+
+ // Ensure the UI for each group is in the correct state.
+ this.onSelectAlwaysTranslateLanguage();
+ this.onSelectNeverTranslateLanguage();
+ this.onSelectNeverTranslateSite();
+ },
+
+ /**
+ * Retrieves the value of a char-pref splits its value into an
+ * array delimited by commas.
+ *
+ * This is used for the translations preferences which are comma-
+ * separated lists of BCP-47 language tags.
+ *
+ * @param {string} pref
+ * @returns {Array<string>}
+ */
+ getLangsFromPref(pref) {
+ let rawLangs = Services.prefs.getCharPref(pref);
+ if (!rawLangs) {
+ return [];
+ }
+
+ let langArr = rawLangs.split(",");
+ let displayNames = Services.intl.getLanguageDisplayNames(
+ undefined,
+ langArr
+ );
+ let langs = langArr.map((lang, i) => new Lang(lang, displayNames[i]));
+ langs.sort();
+
+ return langs;
+ },
+
+ /**
+ * Retrieves the always-translate language tags as an array.
+ * @returns {Array<string>}
+ */
+ getAlwaysTranslateLanguages() {
+ return this.getLangsFromPref(ALWAYS_TRANSLATE_LANGS_PREF);
+ },
+
+ /**
+ * Retrieves the never-translate language tags as an array.
+ * @returns {Array<string>}
+ */
+ getNeverTranslateLanguages() {
+ return this.getLangsFromPref(NEVER_TRANSLATE_LANGS_PREF);
+ },
+
+ /**
+ * Handles updating the UI components on pref or permission changes.
+ */
+ observe(aSubject, aTopic, aData) {
+ if (aTopic === "perm-changed") {
+ if (aData === "cleared") {
+ // Permissions have been cleared
+ if (!this._neverTranslateSites.length) {
+ // There were no sites with permissions set, nothing to do.
+ return;
+ }
+ // Update the tree based on the amount of permissions removed.
+ let removed = this._neverTranslateSites.splice(
+ 0,
+ this._neverTranslateSites.length
+ );
+ this._neverTranslateSiteTree.tree.rowCountChanged(0, -removed.length);
+ } else {
+ let perm = aSubject.QueryInterface(Ci.nsIPermission);
+ if (perm.type != TRANSLATIONS_PERMISSION) {
+ // The updated permission was not for Translations, nothing to do.
+ return;
+ }
+ if (aData === "added") {
+ if (perm.capability != Services.perms.DENY_ACTION) {
+ // We are only showing data for sites we should never translate.
+ // If the permission is not DENY_ACTION, we don't care about it here.
+ return;
+ }
+ this._neverTranslateSites.push(perm.principal.origin);
+ this._neverTranslateSites.sort();
+ let tree = this._neverTranslateSiteTree.tree;
+ tree.rowCountChanged(0, 1);
+ tree.invalidate();
+ } else if (aData == "deleted") {
+ let index = this._neverTranslateSites.indexOf(perm.principal.origin);
+ if (index == -1) {
+ // The deleted permission was not in the tree, nothing to do.
+ return;
+ }
+ this._neverTranslateSites.splice(index, 1);
+ this._neverTranslateSiteTree.tree.rowCountChanged(index, -1);
+ }
+ }
+ // Ensure the UI updates to the changes.
+ this.onSelectNeverTranslateSite();
+ } else if (aTopic === "nsPref:changed") {
+ switch (aData) {
+ case ALWAYS_TRANSLATE_LANGS_PREF: {
+ this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();
+
+ let alwaysTranslateLangsChange =
+ this._alwaysTranslateLangs.length -
+ this._alwaysTranslateLangsTree.rowCount;
+
+ this._alwaysTranslateLangsTree._data = this._alwaysTranslateLangs;
+ let alwaysTranslateLangsTree = this._alwaysTranslateLangsTree.tree;
+
+ if (alwaysTranslateLangsChange) {
+ alwaysTranslateLangsTree.rowCountChanged(
+ 0,
+ alwaysTranslateLangsChange
+ );
+ }
+
+ alwaysTranslateLangsTree.invalidate();
+
+ // Ensure the UI updates to the changes.
+ this.onSelectAlwaysTranslateLanguage();
+ break;
+ }
+ case NEVER_TRANSLATE_LANGS_PREF: {
+ this._neverTranslateLangs = this.getNeverTranslateLanguages();
+
+ let neverTranslateLangsChange =
+ this._neverTranslateLangs.length -
+ this._neverTranslateLangsTree.rowCount;
+
+ this._neverTranslateLangsTree._data = this._neverTranslateLangs;
+ let neverTranslateLangsTree = this._neverTranslateLangsTree.tree;
+
+ if (neverTranslateLangsChange) {
+ neverTranslateLangsTree.rowCountChanged(
+ 0,
+ neverTranslateLangsChange
+ );
+ }
+
+ neverTranslateLangsTree.invalidate();
+
+ // Ensure the UI updates to the changes.
+ this.onSelectNeverTranslateLanguage();
+ break;
+ }
+ }
+ }
+ },
+
+ /**
+ * Ensures that buttons states are enabled/disabled accordingly based on the
+ * content of the trees.
+ *
+ * The remove button should be enabled only if an item is selected.
+ * The removeAll button should be enabled any time the tree has content.
+ *
+ * @param {Tree} aTree
+ * @param {string} aIdPart
+ */
+ _handleButtonDisabling(aTree, aIdPart) {
+ let empty = aTree.isEmpty;
+ document.getElementById("removeAll" + aIdPart + "s").disabled = empty;
+ document.getElementById("remove" + aIdPart).disabled =
+ empty || !aTree.hasSelection;
+ },
+
+ /**
+ * Updates the UI state for the always-translate languages section.
+ */
+ onSelectAlwaysTranslateLanguage() {
+ this._handleButtonDisabling(
+ this._alwaysTranslateLangsTree,
+ "AlwaysTranslateLanguage"
+ );
+ },
+
+ /**
+ * Updates the UI state for the never-translate languages section.
+ */
+ onSelectNeverTranslateLanguage() {
+ this._handleButtonDisabling(
+ this._neverTranslateLangsTree,
+ "NeverTranslateLanguage"
+ );
+ },
+
+ /**
+ * Updates the UI state for the never-translate sites section.
+ */
+ onSelectNeverTranslateSite() {
+ this._handleButtonDisabling(
+ this._neverTranslateSiteTree,
+ "NeverTranslateSite"
+ );
+ },
+
+ /**
+ * Updates the value of a language pref to match when a language is removed
+ * through the UI.
+ *
+ * @param {string} pref
+ * @param {Tree} tree
+ */
+ _onRemoveLanguage(pref, tree) {
+ let langs = Services.prefs.getCharPref(pref);
+ if (!langs) {
+ return;
+ }
+
+ let removed = tree.getSelectedItems().map(l => l.langCode);
+
+ langs = langs.split(",").filter(l => !removed.includes(l));
+ Services.prefs.setCharPref(pref, langs.join(","));
+ },
+
+ /**
+ * Updates the never-translate language pref when a never-translate language
+ * is removed via the UI.
+ */
+ onRemoveAlwaysTranslateLanguage() {
+ this._onRemoveLanguage(
+ ALWAYS_TRANSLATE_LANGS_PREF,
+ this._alwaysTranslateLangsTree
+ );
+ },
+
+ /**
+ * Updates the always-translate language pref when a always-translate language
+ * is removed via the UI.
+ */
+ onRemoveNeverTranslateLanguage() {
+ this._onRemoveLanguage(
+ NEVER_TRANSLATE_LANGS_PREF,
+ this._neverTranslateLangsTree
+ );
+ },
+
+ /**
+ * Updates the permissions for a never-translate site when it is removed via the UI.
+ */
+ onRemoveNeverTranslateSite() {
+ let removedNeverTranslateSites =
+ this._neverTranslateSiteTree.getSelectedItems();
+ for (let origin of removedNeverTranslateSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION);
+ }
+ },
+
+ /**
+ * Clears the always-translate languages pref when the list is cleared in the UI.
+ */
+ onRemoveAllAlwaysTranslateLanguages() {
+ Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "");
+ },
+
+ /**
+ * Clears the never-translate languages pref when the list is cleared in the UI.
+ */
+ onRemoveAllNeverTranslateLanguages() {
+ Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "");
+ },
+
+ /**
+ * Clears the never-translate sites pref when the list is cleared in the UI.
+ */
+ onRemoveAllNeverTranslateSites() {
+ if (this._neverTranslateSiteTree.isEmpty) {
+ return;
+ }
+
+ let removedNeverTranslateSites = this._neverTranslateSites.splice(
+ 0,
+ this._neverTranslateSites.length
+ );
+ this._neverTranslateSiteTree.tree.rowCountChanged(
+ 0,
+ -removedNeverTranslateSites.length
+ );
+
+ for (let origin of removedNeverTranslateSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION);
+ }
+
+ this.onSelectNeverTranslateSite();
+ },
+
+ /**
+ * Handles removing a selected always-translate language via the keyboard.
+ */
+ onAlwaysTranslateLanguageKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onRemoveAlwaysTranslateLanguage();
+ }
+ },
+
+ /**
+ * Handles removing a selected never-translate language via the keyboard.
+ */
+ onNeverTranslateLanguageKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onRemoveNeverTranslateLanguage();
+ }
+ },
+
+ /**
+ * Handles removing a selected never-translate site via the keyboard.
+ */
+ onNeverTranslateSiteKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onRemoveNeverTranslateSite();
+ }
+ },
+
+ /**
+ * Removes any active preference and permissions observers.
+ */
+ removeObservers() {
+ Services.obs.removeObserver(this, "perm-changed");
+ Services.prefs.removeObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
+ Services.prefs.removeObserver(NEVER_TRANSLATE_LANGS_PREF, this);
+ },
+};
diff --git a/browser/components/preferences/dialogs/translations.xhtml b/browser/components/preferences/dialogs/translations.xhtml
new file mode 100644
index 0000000000..205bd54ac1
--- /dev/null
+++ b/browser/components/preferences/dialogs/translations.xhtml
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="TranslationsDialog"
+ data-l10n-id="translations-settings-title"
+ data-l10n-attrs="title, style"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gTranslationsSettings.onLoad();"
+ onunload="gTranslationsSettings.removeObservers();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept"
+ data-l10n-id="translations-settings-close-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/translations.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/translations.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="translations-settings-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <vbox flex="1">
+ <label
+ id="alwaysTranslateLanguagesLabel"
+ data-l10n-id="translations-settings-always-translate-langs-description"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="alwaysTranslateLanguagesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationsSettings.onAlwaysTranslateLanguageKeyPress(event)"
+ onselect="gTranslationsSettings.onSelectAlwaysTranslateLanguage();"
+ >
+ <treecols>
+ <treecol
+ id="languageCol"
+ data-l10n-id="translations-settings-languages-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="start">
+ <button
+ id="removeAlwaysTranslateLanguage"
+ disabled="true"
+ data-l10n-id="translations-settings-remove-language-button"
+ oncommand="gTranslationsSettings.onRemoveAlwaysTranslateLanguage();"
+ />
+ <button
+ id="removeAllAlwaysTranslateLanguages"
+ data-l10n-id="translations-settings-remove-all-languages-button"
+ oncommand="gTranslationsSettings.onRemoveAllAlwaysTranslateLanguages();"
+ />
+ </hbox>
+ <separator />
+ <vbox flex="1">
+ <label
+ id="neverTranslateLanguagesLabel"
+ data-l10n-id="translations-settings-never-translate-langs-description"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="neverTranslateLanguagesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationsSettings.onNeverTranslateLanguageKeyPress(event)"
+ onselect="gTranslationsSettings.onSelectNeverTranslateLanguage();"
+ >
+ <treecols>
+ <treecol
+ id="languageCol"
+ data-l10n-id="translations-settings-languages-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="start">
+ <button
+ id="removeNeverTranslateLanguage"
+ disabled="true"
+ data-l10n-id="translations-settings-remove-language-button"
+ oncommand="gTranslationsSettings.onRemoveNeverTranslateLanguage();"
+ />
+ <button
+ id="removeAllNeverTranslateLanguages"
+ data-l10n-id="translations-settings-remove-all-languages-button"
+ oncommand="gTranslationsSettings.onRemoveAllNeverTranslateLanguages();"
+ />
+ </hbox>
+ <separator />
+ <vbox flex="1">
+ <label
+ id="neverTranslateSitesLabel"
+ data-l10n-id="translations-settings-never-translate-sites-description"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="neverTranslateSitesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationsSettings.onNeverTranslateSiteKeyPress(event)"
+ onselect="gTranslationsSettings.onSelectNeverTranslateSite();"
+ >
+ <treecols>
+ <treecol
+ id="siteCol"
+ data-l10n-id="translations-settings-sites-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="start">
+ <button
+ id="removeNeverTranslateSite"
+ disabled="true"
+ data-l10n-id="translations-settings-remove-site-button"
+ oncommand="gTranslationsSettings.onRemoveNeverTranslateSite();"
+ />
+ <button
+ id="removeAllNeverTranslateSites"
+ data-l10n-id="translations-settings-remove-all-sites-button"
+ oncommand="gTranslationsSettings.onRemoveAllNeverTranslateSites();"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/experimental.inc.xhtml b/browser/components/preferences/experimental.inc.xhtml
new file mode 100644
index 0000000000..67ba010102
--- /dev/null
+++ b/browser/components/preferences/experimental.inc.xhtml
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!-- Experimental panel -->
+
+<script src="chrome://browser/content/preferences/experimental.js"/>
+<html:template id="template-paneExperimental">
+<vbox id="firefoxExperimentalCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneExperimental">
+ <html:h1 style="flex: 1;" data-l10n-id="pane-experimental-title"/>
+ <label><html:h2 id="pane-experimental-subtitle" data-l10n-id="pane-experimental-subtitle"/></label>
+ <hbox pack="end">
+ <button id="experimentalCategory-reset"
+ class="accessory-button"
+ data-l10n-id="pane-experimental-reset"/>
+ </hbox>
+</vbox>
+
+<groupbox data-category="paneExperimental"
+ id="pane-experimental-featureGates"
+ hidden="true">
+ <label class="search-header" hidden="true">
+ <html:h2 id="pane-experimental-search-results-header" data-l10n-id="pane-experimental-search-results-header"/>
+ </label>
+ <html:p data-l10n-id="pane-experimental-description2"/>
+</groupbox>
+</html:template>
+
+<html:template id="template-featureGate">
+ <html:div class="featureGate">
+ <checkbox class="featureGateCheckbox"/>
+ <label class="featureGateDescription"/>
+ </html:div>
+</html:template>
diff --git a/browser/components/preferences/experimental.js b/browser/components/preferences/experimental.js
new file mode 100644
index 0000000000..266aeabc4f
--- /dev/null
+++ b/browser/components/preferences/experimental.js
@@ -0,0 +1,163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from preferences.js */
+
+var gExperimentalPane = {
+ inited: false,
+ _template: null,
+ _featureGatesContainer: null,
+ _boundRestartObserver: null,
+ _observedPrefs: [],
+ _shouldPromptForRestart: true,
+
+ _featureGatePrefTypeToPrefServiceType(featureGatePrefType) {
+ if (featureGatePrefType != "boolean") {
+ throw new Error("Only boolean FeatureGates are supported");
+ }
+ return "bool";
+ },
+
+ async _observeRestart(aSubject, aTopic, aData) {
+ if (!this._shouldPromptForRestart) {
+ return;
+ }
+ let prefValue = Services.prefs.getBoolPref(aData);
+ let buttonIndex = await confirmRestartPrompt(prefValue, 1, true, false);
+ if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ return;
+ }
+ this._shouldPromptForRestart = false;
+ Services.prefs.setBoolPref(aData, !prefValue);
+ this._shouldPromptForRestart = true;
+ },
+
+ addPrefObserver(name, fn) {
+ this._observedPrefs.push({ name, fn });
+ Services.prefs.addObserver(name, fn);
+ },
+
+ removePrefObservers() {
+ for (let { name, fn } of this._observedPrefs) {
+ Services.prefs.removeObserver(name, fn);
+ }
+ this._observedPrefs = [];
+ },
+
+ // Reset the features to their default values
+ async resetAllFeatures() {
+ let features = await gExperimentalPane.getFeatures();
+ for (let feature of features) {
+ Services.prefs.setBoolPref(feature.preference, feature.defaultValue);
+ }
+ },
+
+ async getFeatures() {
+ let searchParams = new URLSearchParams(document.documentURIObject.query);
+ let definitionsUrl = searchParams.get("definitionsUrl");
+ let features = await FeatureGate.all(definitionsUrl);
+ return features.filter(f => f.isPublic);
+ },
+
+ async _sortFeatures(features) {
+ // Sort the features alphabetically by their title
+ let titles = await document.l10n.formatMessages(
+ features.map(f => {
+ return { id: f.title };
+ })
+ );
+ titles = titles.map((title, index) => [title.attributes[0].value, index]);
+ titles.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()));
+ // Get the features in order of sorted titles.
+ return titles.map(([, index]) => features[index]);
+ },
+
+ async init() {
+ if (this.inited) {
+ return;
+ }
+ this.inited = true;
+
+ let features = await this.getFeatures();
+ let shouldHide = !features.length;
+ document.getElementById("category-experimental").hidden = shouldHide;
+ // Cache the visibility so we can show it quicker in subsequent loads.
+ Services.prefs.setBoolPref(
+ "browser.preferences.experimental.hidden",
+ shouldHide
+ );
+ if (shouldHide) {
+ // Remove the 'experimental' category if there are no available features
+ document.getElementById("firefoxExperimentalCategory").remove();
+ if (
+ document.getElementById("categories").selectedItem?.id ==
+ "category-experimental"
+ ) {
+ // Leave the 'experimental' category if there are no available features
+ gotoPref("general");
+ return;
+ }
+ }
+
+ features = await this._sortFeatures(features);
+
+ setEventListener(
+ "experimentalCategory-reset",
+ "command",
+ gExperimentalPane.resetAllFeatures
+ );
+
+ window.addEventListener("unload", () => this.removePrefObservers());
+ this._template = document.getElementById("template-featureGate");
+ this._featureGatesContainer = document.getElementById(
+ "pane-experimental-featureGates"
+ );
+ this._boundRestartObserver = this._observeRestart.bind(this);
+ let frag = document.createDocumentFragment();
+ for (let feature of features) {
+ if (Preferences.get(feature.preference)) {
+ console.error(
+ "Preference control already exists for experimental feature '" +
+ feature.id +
+ "' with preference '" +
+ feature.preference +
+ "'"
+ );
+ continue;
+ }
+ if (feature.restartRequired) {
+ this.addPrefObserver(feature.preference, this._boundRestartObserver);
+ }
+ let template = this._template.content.cloneNode(true);
+ let description = template.querySelector(".featureGateDescription");
+ description.id = feature.id + "-description";
+ let descriptionLinks = feature.descriptionLinks || {};
+ for (let [key, value] of Object.entries(descriptionLinks)) {
+ let link = document.createElement("a");
+ link.setAttribute("data-l10n-name", key);
+ link.setAttribute("href", value);
+ link.setAttribute("target", "_blank");
+ description.append(link);
+ }
+ document.l10n.setAttributes(description, feature.description);
+ let checkbox = template.querySelector(".featureGateCheckbox");
+ checkbox.setAttribute("preference", feature.preference);
+ checkbox.id = feature.id;
+ checkbox.setAttribute("aria-describedby", description.id);
+ document.l10n.setAttributes(checkbox, feature.title);
+ frag.appendChild(template);
+ let preference = Preferences.add({
+ id: feature.preference,
+ type: gExperimentalPane._featureGatePrefTypeToPrefServiceType(
+ feature.type
+ ),
+ });
+ preference.setElementValue(checkbox);
+ }
+ this._featureGatesContainer.appendChild(frag);
+ },
+};
diff --git a/browser/components/preferences/extensionControlled.js b/browser/components/preferences/extensionControlled.js
new file mode 100644
index 0000000000..3c6f78ab42
--- /dev/null
+++ b/browser/components/preferences/extensionControlled.js
@@ -0,0 +1,309 @@
+/* - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from preferences.js */
+
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+// Note: we get loaded in dialogs so we need to define our
+// own getters, separate from preferences.js .
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+
+ ExtensionPreferencesManager:
+ "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
+
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+const PREF_SETTING_TYPE = "prefs";
+const PROXY_KEY = "proxy.settings";
+const API_PROXY_PREFS = [
+ "network.proxy.type",
+ "network.proxy.http",
+ "network.proxy.http_port",
+ "network.proxy.share_proxy_settings",
+ "network.proxy.ssl",
+ "network.proxy.ssl_port",
+ "network.proxy.socks",
+ "network.proxy.socks_port",
+ "network.proxy.socks_version",
+ "network.proxy.socks_remote_dns",
+ "network.proxy.no_proxies_on",
+ "network.proxy.autoconfig_url",
+ "signon.autologin.proxy",
+];
+
+let extensionControlledContentIds = {
+ "privacy.containers": "browserContainersExtensionContent",
+ webNotificationsDisabled: "browserNotificationsPermissionExtensionContent",
+ "services.passwordSavingEnabled": "passwordManagerExtensionContent",
+ "proxy.settings": "proxyExtensionContent",
+ get "websites.trackingProtectionMode"() {
+ return {
+ button: "contentBlockingDisableTrackingProtectionExtension",
+ section: "contentBlockingTrackingProtectionExtensionContentLabel",
+ };
+ },
+};
+
+const extensionControlledL10nKeys = {
+ webNotificationsDisabled: "web-notifications",
+ "services.passwordSavingEnabled": "password-saving",
+ "privacy.containers": "privacy-containers",
+ "websites.trackingProtectionMode": "websites-content-blocking-all-trackers",
+ "proxy.settings": "proxy-config",
+};
+
+let extensionControlledIds = {};
+
+/**
+ * Check if a pref is being managed by an extension.
+ */
+async function getControllingExtensionInfo(type, settingName) {
+ await ExtensionSettingsStore.initialize();
+ return ExtensionSettingsStore.getSetting(type, settingName);
+}
+
+function getControllingExtensionEls(settingName) {
+ let idInfo = extensionControlledContentIds[settingName];
+ let section = document.getElementById(idInfo.section || idInfo);
+ let button = idInfo.button
+ ? document.getElementById(idInfo.button)
+ : section.querySelector("button");
+ return {
+ section,
+ button,
+ description: section.querySelector("description"),
+ };
+}
+
+async function getControllingExtension(type, settingName) {
+ let info = await getControllingExtensionInfo(type, settingName);
+ let addon = info && info.id && (await AddonManager.getAddonByID(info.id));
+ return addon;
+}
+
+async function handleControllingExtension(type, settingName) {
+ let addon = await getControllingExtension(type, settingName);
+
+ // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
+ // an extension is controlling a setting but the extension has been uninstalled
+ // outside of the regular lifecycle. If the extension isn't currently installed
+ // then we should treat the setting as not being controlled.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
+ if (addon) {
+ extensionControlledIds[settingName] = addon.id;
+ showControllingExtension(settingName, addon);
+ } else {
+ let elements = getControllingExtensionEls(settingName);
+ if (
+ extensionControlledIds[settingName] &&
+ !document.hidden &&
+ elements.button
+ ) {
+ showEnableExtensionMessage(settingName);
+ } else {
+ hideControllingExtension(settingName);
+ }
+ delete extensionControlledIds[settingName];
+ }
+
+ return !!addon;
+}
+
+function settingNameToL10nID(settingName) {
+ if (!extensionControlledL10nKeys.hasOwnProperty(settingName)) {
+ throw new Error(
+ `Unknown extension controlled setting name: ${settingName}`
+ );
+ }
+ return `extension-controlling-${extensionControlledL10nKeys[settingName]}`;
+}
+
+/**
+ * Set the localization data for the description of the controlling extension.
+ *
+ * The function alters the inner DOM structure of the fragment to, depending
+ * on the `addon` argument, remove the `<img/>` element or ensure it's
+ * set to the correct src.
+ * This allows Fluent DOM Overlays to localize the fragment.
+ *
+ * @param elem {Element}
+ * <description> element to be annotated
+ * @param addon {Object?}
+ * Addon object with meta information about the addon (or null)
+ * @param settingName {String}
+ * If `addon` is set this handled the name of the setting that will be used
+ * to fetch the l10n id for the given message.
+ * If `addon` is set to null, this will be the full l10n-id assigned to the
+ * element.
+ */
+function setControllingExtensionDescription(elem, addon, settingName) {
+ const existingImg = elem.querySelector("img");
+ if (addon === null) {
+ // If the element has an image child element,
+ // remove it.
+ if (existingImg) {
+ existingImg.remove();
+ }
+ document.l10n.setAttributes(elem, settingName);
+ return;
+ }
+
+ const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ const src = addon.iconURL || defaultIcon;
+
+ if (!existingImg) {
+ // If an element doesn't have an image child
+ // node, add it.
+ let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
+ image.setAttribute("src", src);
+ image.setAttribute("data-l10n-name", "icon");
+ image.setAttribute("role", "presentation");
+ image.classList.add("extension-controlled-icon");
+ elem.appendChild(image);
+ } else if (existingImg.getAttribute("src") !== src) {
+ existingImg.setAttribute("src", src);
+ }
+
+ const l10nId = settingNameToL10nID(settingName);
+ document.l10n.setAttributes(elem, l10nId, {
+ name: addon.name,
+ });
+}
+
+async function showControllingExtension(settingName, addon) {
+ // Tell the user what extension is controlling the setting.
+ let elements = getControllingExtensionEls(settingName);
+
+ elements.section.classList.remove("extension-controlled-disabled");
+ let description = elements.description;
+
+ setControllingExtensionDescription(description, addon, settingName);
+
+ if (elements.button) {
+ elements.button.hidden = false;
+ }
+
+ // Show the controlling extension row and hide the old label.
+ elements.section.hidden = false;
+}
+
+function hideControllingExtension(settingName) {
+ let elements = getControllingExtensionEls(settingName);
+ elements.section.hidden = true;
+ if (elements.button) {
+ elements.button.hidden = true;
+ }
+}
+
+function showEnableExtensionMessage(settingName) {
+ let elements = getControllingExtensionEls(settingName);
+
+ elements.button.hidden = true;
+ elements.section.classList.add("extension-controlled-disabled");
+
+ elements.description.textContent = "";
+
+ // We replace localization of the <description> with a DOM Fragment containing
+ // the enable-extension-enable message. That means a change from:
+ //
+ // <description data-l10n-id="..."/>
+ //
+ // to:
+ //
+ // <description>
+ // <img/>
+ // <label data-l10n-id="..."/>
+ // </description>
+ //
+ // We need to remove the l10n-id annotation from the <description> to prevent
+ // Fluent from overwriting the element in case of any retranslation.
+ elements.description.removeAttribute("data-l10n-id");
+
+ let icon = (url, name) => {
+ let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
+ img.src = url;
+ img.setAttribute("data-l10n-name", name);
+ img.setAttribute("role", "presentation");
+ img.className = "extension-controlled-icon";
+ return img;
+ };
+ let label = document.createXULElement("label");
+ let addonIcon = icon(
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg",
+ "addons-icon"
+ );
+ let toolbarIcon = icon("chrome://browser/skin/menu.svg", "menu-icon");
+ label.appendChild(addonIcon);
+ label.appendChild(toolbarIcon);
+ document.l10n.setAttributes(label, "extension-controlled-enable");
+ elements.description.appendChild(label);
+ let dismissButton = document.createXULElement("image");
+ dismissButton.setAttribute("class", "extension-controlled-icon close-icon");
+ dismissButton.addEventListener("click", function dismissHandler() {
+ hideControllingExtension(settingName);
+ dismissButton.removeEventListener("click", dismissHandler);
+ });
+ elements.description.appendChild(dismissButton);
+}
+
+function makeDisableControllingExtension(type, settingName) {
+ return async function disableExtension() {
+ let { id } = await getControllingExtensionInfo(type, settingName);
+ let addon = await AddonManager.getAddonByID(id);
+ await addon.disable();
+ };
+}
+
+/**
+ * Initialize listeners though the Management API to update the UI
+ * when an extension is controlling a pref.
+ * @param {string} type
+ * @param {string} prefId The unique id of the setting
+ * @param {HTMLElement} controlledElement
+ */
+async function initListenersForPrefChange(type, prefId, controlledElement) {
+ await Management.asyncLoadSettingsModules();
+
+ let managementObserver = async () => {
+ let managementControlled = await handleControllingExtension(type, prefId);
+ // Enterprise policy may have locked the pref, so we need to preserve that
+ controlledElement.disabled =
+ managementControlled || Services.prefs.prefIsLocked(prefId);
+ };
+ managementObserver();
+ Management.on(`extension-setting-changed:${prefId}`, managementObserver);
+
+ window.addEventListener("unload", () => {
+ Management.off(`extension-setting-changed:${prefId}`, managementObserver);
+ });
+}
+
+function initializeProxyUI(container) {
+ let deferredUpdate = new DeferredTask(() => {
+ container.updateProxySettingsUI();
+ }, 10);
+ let proxyObserver = {
+ observe: (subject, topic, data) => {
+ if (API_PROXY_PREFS.includes(data)) {
+ deferredUpdate.arm();
+ }
+ },
+ };
+ Services.prefs.addObserver("", proxyObserver);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver("", proxyObserver);
+ });
+}
diff --git a/browser/components/preferences/findInPage.js b/browser/components/preferences/findInPage.js
new file mode 100644
index 0000000000..26b6f23846
--- /dev/null
+++ b/browser/components/preferences/findInPage.js
@@ -0,0 +1,772 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+
+// A tweak to the standard <button> CE to use textContent on the <label>
+// inside the button, which allows the text to be highlighted when the user
+// is searching.
+
+const MozButton = customElements.get("button");
+class HighlightableButton extends MozButton {
+ static get inheritedAttributes() {
+ return Object.assign({}, super.inheritedAttributes, {
+ ".button-text": "text=label,accesskey,crop",
+ });
+ }
+}
+customElements.define("highlightable-button", HighlightableButton, {
+ extends: "button",
+});
+
+var gSearchResultsPane = {
+ listSearchTooltips: new Set(),
+ listSearchMenuitemIndicators: new Set(),
+ searchInput: null,
+ // A map of DOM Elements to a string of keywords used in search
+ // XXX: We should invalidate this cache on `intl:app-locales-changed`
+ searchKeywords: new WeakMap(),
+ inited: false,
+
+ // A (node -> boolean) map of subitems to be made visible or hidden.
+ subItems: new Map(),
+
+ searchResultsHighlighted: false,
+
+ init() {
+ if (this.inited) {
+ return;
+ }
+ this.inited = true;
+ this.searchInput = document.getElementById("searchInput");
+ this.searchInput.hidden = !Services.prefs.getBoolPref(
+ "browser.preferences.search"
+ );
+
+ window.addEventListener("resize", () => {
+ this._recomputeTooltipPositions();
+ });
+
+ if (!this.searchInput.hidden) {
+ this.searchInput.addEventListener("input", this);
+ this.searchInput.addEventListener("command", this);
+ window.addEventListener("DOMContentLoaded", () => {
+ this.searchInput.focus();
+ // Initialize other panes in an idle callback.
+ window.requestIdleCallback(() => this.initializeCategories());
+ });
+ }
+ ensureScrollPadding();
+ },
+
+ async handleEvent(event) {
+ // Ensure categories are initialized if idle callback didn't run sooo enough.
+ await this.initializeCategories();
+ this.searchFunction(event);
+ },
+
+ /**
+ * This stops the search input from moving, when typing in it
+ * changes which items in the prefs are visible.
+ */
+ fixInputPosition() {
+ let innerContainer = document.querySelector(".sticky-inner-container");
+ let width =
+ window.windowUtils.getBoundsWithoutFlushing(innerContainer).width;
+ innerContainer.style.maxWidth = width + "px";
+ },
+
+ /**
+ * Check that the text content contains the query string.
+ *
+ * @param String content
+ * the text content to be searched
+ * @param String query
+ * the query string
+ * @returns boolean
+ * true when the text content contains the query string else false
+ */
+ queryMatchesContent(content, query) {
+ if (!content || !query) {
+ return false;
+ }
+ return content.toLowerCase().includes(query.toLowerCase());
+ },
+
+ categoriesInitialized: false,
+
+ /**
+ * Will attempt to initialize all uninitialized categories
+ */
+ async initializeCategories() {
+ // Initializing all the JS for all the tabs
+ if (!this.categoriesInitialized) {
+ this.categoriesInitialized = true;
+ // Each element of gCategoryInits is a name
+ for (let [, /* name */ category] of gCategoryInits) {
+ if (!category.inited) {
+ await category.init();
+ }
+ }
+ }
+ },
+
+ /**
+ * Finds and returns text nodes within node and all descendants
+ * Iterates through all the sibilings of the node object and adds the sibilings
+ * to an array if sibiling is a TEXT_NODE else checks the text nodes with in current node
+ * Source - http://stackoverflow.com/questions/10730309/find-all-text-nodes-in-html-page
+ *
+ * @param Node nodeObject
+ * DOM element
+ * @returns array of text nodes
+ */
+ textNodeDescendants(node) {
+ if (!node) {
+ return [];
+ }
+ let all = [];
+ for (node = node.firstChild; node; node = node.nextSibling) {
+ if (node.nodeType === node.TEXT_NODE) {
+ all.push(node);
+ } else {
+ all = all.concat(this.textNodeDescendants(node));
+ }
+ }
+ return all;
+ },
+
+ /**
+ * This function is used to find words contained within the text nodes.
+ * We pass in the textNodes because they contain the text to be highlighted.
+ * We pass in the nodeSizes to tell exactly where highlighting need be done.
+ * When creating the range for highlighting, if the nodes are section is split
+ * by an access key, it is important to have the size of each of the nodes summed.
+ * @param Array textNodes
+ * List of DOM elements
+ * @param Array nodeSizes
+ * Running size of text nodes. This will contain the same number of elements as textNodes.
+ * The first element is the size of first textNode element.
+ * For any nodes after, they will contain the summation of the nodes thus far in the array.
+ * Example:
+ * textNodes = [[This is ], [a], [n example]]
+ * nodeSizes = [[8], [9], [18]]
+ * This is used to determine the offset when highlighting
+ * @param String textSearch
+ * Concatination of textNodes's text content
+ * Example:
+ * textNodes = [[This is ], [a], [n example]]
+ * nodeSizes = "This is an example"
+ * This is used when executing the regular expression
+ * @param String searchPhrase
+ * word or words to search for
+ * @returns boolean
+ * Returns true when atleast one instance of search phrase is found, otherwise false
+ */
+ highlightMatches(textNodes, nodeSizes, textSearch, searchPhrase) {
+ if (!searchPhrase) {
+ return false;
+ }
+
+ let indices = [];
+ let i = -1;
+ while ((i = textSearch.indexOf(searchPhrase, i + 1)) >= 0) {
+ indices.push(i);
+ }
+
+ // Looping through each spot the searchPhrase is found in the concatenated string
+ for (let startValue of indices) {
+ let endValue = startValue + searchPhrase.length;
+ let startNode = null;
+ let endNode = null;
+ let nodeStartIndex = null;
+
+ // Determining the start and end node to highlight from
+ for (let index = 0; index < nodeSizes.length; index++) {
+ let lengthNodes = nodeSizes[index];
+ // Determining the start node
+ if (!startNode && lengthNodes >= startValue) {
+ startNode = textNodes[index];
+ nodeStartIndex = index;
+ // Calculating the offset when found query is not in the first node
+ if (index > 0) {
+ startValue -= nodeSizes[index - 1];
+ }
+ }
+ // Determining the end node
+ if (!endNode && lengthNodes >= endValue) {
+ endNode = textNodes[index];
+ // Calculating the offset when endNode is different from startNode
+ // or when endNode is not the first node
+ if (index != nodeStartIndex || index > 0) {
+ endValue -= nodeSizes[index - 1];
+ }
+ }
+ }
+ let range = document.createRange();
+ range.setStart(startNode, startValue);
+ range.setEnd(endNode, endValue);
+ this.getFindSelection(startNode.ownerGlobal).addRange(range);
+
+ this.searchResultsHighlighted = true;
+ }
+
+ return !!indices.length;
+ },
+
+ /**
+ * Get the selection instance from given window
+ *
+ * @param Object win
+ * The window object points to frame's window
+ */
+ getFindSelection(win) {
+ // Yuck. See bug 138068.
+ let docShell = win.docShell;
+
+ let controller = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+
+ let selection = controller.getSelection(
+ Ci.nsISelectionController.SELECTION_FIND
+ );
+ selection.setColors("currentColor", "#ffe900", "currentColor", "#003eaa");
+
+ return selection;
+ },
+
+ /**
+ * Shows or hides content according to search input
+ *
+ * @param String event
+ * to search for filted query in
+ */
+ async searchFunction(event) {
+ let query = event.target.value.trim().toLowerCase();
+ if (this.query == query) {
+ return;
+ }
+
+ let firstQuery = !this.query && query;
+ let endQuery = !query && this.query;
+ let subQuery = this.query && query.includes(this.query);
+ this.query = query;
+
+ // If there is a query, don't reshow the existing hidden subitems yet
+ // to avoid them flickering into view only to be hidden again by
+ // this next search.
+ this.removeAllSearchIndicators(window, !query.length);
+
+ // Clear telemetry request if user types very frequently.
+ if (this.telemetryTimer) {
+ clearTimeout(this.telemetryTimer);
+ }
+
+ let srHeader = document.getElementById("header-searchResults");
+ let noResultsEl = document.getElementById("no-results-message");
+ if (this.query) {
+ // If this is the first query, fix the search input in place.
+ if (firstQuery) {
+ this.fixInputPosition();
+ }
+ // Showing the Search Results Tag
+ await gotoPref("paneSearchResults");
+ srHeader.hidden = false;
+
+ let resultsFound = false;
+
+ // Building the range for highlighted areas
+ let rootPreferencesChildren = [
+ ...document.querySelectorAll(
+ "#mainPrefPane > *:not([data-hidden-from-search], script, stringbundle)"
+ ),
+ ];
+
+ if (subQuery) {
+ // Since the previous query is a subset of the current query,
+ // there is no need to check elements that is hidden already.
+ rootPreferencesChildren = rootPreferencesChildren.filter(
+ el => !el.hidden
+ );
+ }
+
+ // Attach the bindings for all children if they were not already visible.
+ for (let child of rootPreferencesChildren) {
+ if (child.hidden) {
+ child.classList.add("visually-hidden");
+ child.hidden = false;
+ }
+ }
+
+ let ts = performance.now();
+ let FRAME_THRESHOLD = 1000 / 60;
+
+ // Showing or Hiding specific section depending on if words in query are found
+ for (let child of rootPreferencesChildren) {
+ if (performance.now() - ts > FRAME_THRESHOLD) {
+ // Creating tooltips for all the instances found
+ for (let anchorNode of this.listSearchTooltips) {
+ this.createSearchTooltip(anchorNode, this.query);
+ }
+ ts = await new Promise(resolve =>
+ window.requestAnimationFrame(resolve)
+ );
+ if (query !== this.query) {
+ return;
+ }
+ }
+
+ if (
+ !child.classList.contains("header") &&
+ !child.classList.contains("subcategory") &&
+ (await this.searchWithinNode(child, this.query))
+ ) {
+ child.classList.remove("visually-hidden");
+
+ // Show the preceding search-header if one exists.
+ let groupbox = child.closest("groupbox");
+ let groupHeader =
+ groupbox && groupbox.querySelector(".search-header");
+ if (groupHeader) {
+ groupHeader.hidden = false;
+ }
+
+ resultsFound = true;
+ } else {
+ child.classList.add("visually-hidden");
+ }
+ }
+
+ // Hide any subitems that don't match the search term and show
+ // only those that do.
+ if (this.subItems.size) {
+ for (let [subItem, matches] of this.subItems) {
+ subItem.classList.toggle("visually-hidden", !matches);
+ }
+ }
+
+ noResultsEl.hidden = !!resultsFound;
+ noResultsEl.setAttribute("query", this.query);
+ // XXX: This is potentially racy in case where Fluent retranslates the
+ // message and ereases the query within.
+ // The feature is not yet supported, but we should fix for it before
+ // we enable it. See bug 1446389 for details.
+ let msgQueryElem = document.getElementById("sorry-message-query");
+ msgQueryElem.textContent = this.query;
+ if (resultsFound) {
+ // Creating tooltips for all the instances found
+ for (let anchorNode of this.listSearchTooltips) {
+ this.createSearchTooltip(anchorNode, this.query);
+ }
+
+ // Implant search telemetry probe after user stops typing for a while
+ if (this.query.length >= 2) {
+ this.telemetryTimer = setTimeout(() => {
+ Services.telemetry.keyedScalarAdd(
+ "preferences.search_query",
+ this.query,
+ 1
+ );
+ }, 1000);
+ }
+ }
+ } else {
+ if (endQuery) {
+ document
+ .querySelector(".sticky-inner-container")
+ .style.removeProperty("max-width");
+ }
+ noResultsEl.hidden = true;
+ document.getElementById("sorry-message-query").textContent = "";
+ // Going back to General when cleared
+ await gotoPref("paneGeneral");
+ srHeader.hidden = true;
+
+ // Hide some special second level headers in normal view
+ for (let element of document.querySelectorAll(".search-header")) {
+ element.hidden = true;
+ }
+ }
+
+ window.dispatchEvent(
+ new CustomEvent("PreferencesSearchCompleted", { detail: query })
+ );
+ },
+
+ /**
+ * Finding leaf nodes and checking their content for words to search,
+ * It is a recursive function
+ *
+ * @param Node nodeObject
+ * DOM Element
+ * @param String searchPhrase
+ * @returns boolean
+ * Returns true when found in at least one childNode, false otherwise
+ */
+ async searchWithinNode(nodeObject, searchPhrase) {
+ let matchesFound = false;
+ if (
+ nodeObject.childElementCount == 0 ||
+ nodeObject.tagName == "button" ||
+ nodeObject.tagName == "label" ||
+ nodeObject.tagName == "description" ||
+ nodeObject.tagName == "menulist" ||
+ nodeObject.tagName == "menuitem"
+ ) {
+ let simpleTextNodes = this.textNodeDescendants(nodeObject);
+ for (let node of simpleTextNodes) {
+ let result = this.highlightMatches(
+ [node],
+ [node.length],
+ node.textContent.toLowerCase(),
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+
+ // Collecting data from anonymous content / label / description
+ let nodeSizes = [];
+ let allNodeText = "";
+ let runningSize = 0;
+
+ let accessKeyTextNodes = [];
+
+ if (
+ nodeObject.tagName == "label" ||
+ nodeObject.tagName == "description"
+ ) {
+ accessKeyTextNodes.push(...simpleTextNodes);
+ }
+
+ for (let node of accessKeyTextNodes) {
+ runningSize += node.textContent.length;
+ allNodeText += node.textContent;
+ nodeSizes.push(runningSize);
+ }
+
+ // Access key are presented
+ let complexTextNodesResult = this.highlightMatches(
+ accessKeyTextNodes,
+ nodeSizes,
+ allNodeText.toLowerCase(),
+ searchPhrase
+ );
+
+ // Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text.
+ let labelResult = this.queryMatchesContent(
+ nodeObject.getAttribute("label"),
+ searchPhrase
+ );
+
+ // Searching some elements, such as xul:label, store their user-visible text in a "value" attribute.
+ // Value will be skipped for menuitem since value in menuitem could represent index number to distinct each item.
+ let valueResult =
+ nodeObject.tagName !== "menuitem" && nodeObject.tagName !== "radio"
+ ? this.queryMatchesContent(
+ nodeObject.getAttribute("value"),
+ searchPhrase
+ )
+ : false;
+
+ // Searching some elements, such as xul:button, buttons to open subdialogs
+ // using l10n ids.
+ let keywordsResult =
+ nodeObject.hasAttribute("search-l10n-ids") &&
+ (await this.matchesSearchL10nIDs(nodeObject, searchPhrase));
+
+ if (!keywordsResult) {
+ // Searching some elements, such as xul:button, buttons to open subdialogs
+ // using searchkeywords attribute.
+ keywordsResult =
+ !keywordsResult &&
+ nodeObject.hasAttribute("searchkeywords") &&
+ this.queryMatchesContent(
+ nodeObject.getAttribute("searchkeywords"),
+ searchPhrase
+ );
+ }
+
+ // Creating tooltips for buttons
+ if (
+ keywordsResult &&
+ (nodeObject.tagName === "button" || nodeObject.tagName == "menulist")
+ ) {
+ this.listSearchTooltips.add(nodeObject);
+ }
+
+ if (keywordsResult && nodeObject.tagName === "menuitem") {
+ nodeObject.setAttribute("indicator", "true");
+ this.listSearchMenuitemIndicators.add(nodeObject);
+ let menulist = nodeObject.closest("menulist");
+
+ menulist.setAttribute("indicator", "true");
+ this.listSearchMenuitemIndicators.add(menulist);
+ }
+
+ if (
+ (nodeObject.tagName == "menulist" ||
+ nodeObject.tagName == "menuitem") &&
+ (labelResult || valueResult || keywordsResult)
+ ) {
+ nodeObject.setAttribute("highlightable", "true");
+ }
+
+ matchesFound =
+ matchesFound ||
+ complexTextNodesResult ||
+ labelResult ||
+ valueResult ||
+ keywordsResult;
+ }
+
+ // Should not search unselected child nodes of a <xul:deck> element
+ // except the "historyPane" <xul:deck> element.
+ if (nodeObject.tagName == "deck" && nodeObject.id != "historyPane") {
+ let index = nodeObject.selectedIndex;
+ if (index != -1) {
+ let result = await this.searchChildNodeIfVisible(
+ nodeObject,
+ index,
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+ } else {
+ for (let i = 0; i < nodeObject.childNodes.length; i++) {
+ let result = await this.searchChildNodeIfVisible(
+ nodeObject,
+ i,
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+ }
+ return matchesFound;
+ },
+
+ /**
+ * Search for a phrase within a child node if it is visible.
+ *
+ * @param Node nodeObject
+ * The parent DOM Element
+ * @param Number index
+ * The index for the childNode
+ * @param String searchPhrase
+ * @returns boolean
+ * Returns true when found the specific childNode, false otherwise
+ */
+ async searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
+ let result = false;
+ let child = nodeObject.childNodes[index];
+ if (
+ !child.hidden &&
+ nodeObject.getAttribute("data-hidden-from-search") !== "true"
+ ) {
+ result = await this.searchWithinNode(child, searchPhrase);
+ // Creating tooltips for menulist element
+ if (result && nodeObject.tagName === "menulist") {
+ this.listSearchTooltips.add(nodeObject);
+ }
+
+ // If this is a node for an experimental feature option or a Mozilla product item,
+ // add it to the list of subitems. The items that don't match the search term
+ // will be hidden.
+ if (
+ Element.isInstance(child) &&
+ (child.classList.contains("featureGate") ||
+ child.classList.contains("mozilla-product-item"))
+ ) {
+ this.subItems.set(child, result);
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Search for a phrase in l10n messages associated with the element.
+ *
+ * @param Node nodeObject
+ * The parent DOM Element
+ * @param String searchPhrase
+ * @returns boolean
+ * true when the text content contains the query string else false
+ */
+ async matchesSearchL10nIDs(nodeObject, searchPhrase) {
+ if (!this.searchKeywords.has(nodeObject)) {
+ // The `search-l10n-ids` attribute is a comma-separated list of
+ // l10n ids. It may also uses a dot notation to specify an attribute
+ // of the message to be used.
+ //
+ // Example: "containers-add-button.label, user-context-personal"
+ //
+ // The result is an array of arrays of l10n ids and optionally attribute names.
+ //
+ // Example: [["containers-add-button", "label"], ["user-context-personal"]]
+ const refs = nodeObject
+ .getAttribute("search-l10n-ids")
+ .split(",")
+ .map(s => s.trim().split("."))
+ .filter(s => !!s[0].length);
+
+ const messages = await document.l10n.formatMessages(
+ refs.map(ref => ({ id: ref[0] }))
+ );
+
+ // Map the localized messages taking value or a selected attribute and
+ // building a string of concatenated translated strings out of it.
+ let keywords = messages
+ .map((msg, i) => {
+ let [refId, refAttr] = refs[i];
+ if (!msg) {
+ console.error(`Missing search l10n id "${refId}"`);
+ return null;
+ }
+ if (refAttr) {
+ let attr =
+ msg.attributes && msg.attributes.find(a => a.name === refAttr);
+ if (!attr) {
+ console.error(`Missing search l10n id "${refId}.${refAttr}"`);
+ return null;
+ }
+ if (attr.value === "") {
+ console.error(
+ `Empty value added to search-l10n-ids "${refId}.${refAttr}"`
+ );
+ }
+ return attr.value;
+ }
+ if (msg.value === "") {
+ console.error(`Empty value added to search-l10n-ids "${refId}"`);
+ }
+ return msg.value;
+ })
+ .filter(keyword => keyword !== null)
+ .join(" ");
+
+ this.searchKeywords.set(nodeObject, keywords);
+ return this.queryMatchesContent(keywords, searchPhrase);
+ }
+
+ return this.queryMatchesContent(
+ this.searchKeywords.get(nodeObject),
+ searchPhrase
+ );
+ },
+
+ /**
+ * Inserting a div structure infront of the DOM element matched textContent.
+ * Then calculation the offsets to position the tooltip in the correct place.
+ *
+ * @param Node anchorNode
+ * DOM Element
+ * @param String query
+ * Word or words that are being searched for
+ */
+ createSearchTooltip(anchorNode, query) {
+ if (anchorNode.tooltipNode) {
+ return;
+ }
+ let searchTooltip = anchorNode.ownerDocument.createElement("span");
+ let searchTooltipText = anchorNode.ownerDocument.createElement("span");
+ searchTooltip.className = "search-tooltip";
+ searchTooltipText.textContent = query;
+ searchTooltip.appendChild(searchTooltipText);
+
+ // Set tooltipNode property to track corresponded tooltip node.
+ anchorNode.tooltipNode = searchTooltip;
+ anchorNode.parentElement.classList.add("search-tooltip-parent");
+ anchorNode.parentElement.appendChild(searchTooltip);
+
+ this._applyTooltipPosition(
+ searchTooltip,
+ this._computeTooltipPosition(anchorNode, searchTooltip)
+ );
+ },
+
+ _recomputeTooltipPositions() {
+ let positions = [];
+ for (let anchorNode of this.listSearchTooltips) {
+ let searchTooltip = anchorNode.tooltipNode;
+ if (!searchTooltip) {
+ continue;
+ }
+ let position = this._computeTooltipPosition(anchorNode, searchTooltip);
+ positions.push({ searchTooltip, position });
+ }
+ for (let { searchTooltip, position } of positions) {
+ this._applyTooltipPosition(searchTooltip, position);
+ }
+ },
+
+ _applyTooltipPosition(searchTooltip, position) {
+ searchTooltip.style.left = position.left + "px";
+ searchTooltip.style.top = position.top + "px";
+ },
+
+ _computeTooltipPosition(anchorNode, searchTooltip) {
+ // In order to get the up-to-date position of each of the nodes that we're
+ // putting tooltips on, we have to flush layout intentionally. Once
+ // menulists don't use XUL layout we can remove this and use plain CSS to
+ // position them, see bug 1363730.
+ let anchorRect = anchorNode.getBoundingClientRect();
+ let containerRect = anchorNode.parentElement.getBoundingClientRect();
+ let tooltipRect = searchTooltip.getBoundingClientRect();
+
+ let left =
+ anchorRect.left -
+ containerRect.left +
+ anchorRect.width / 2 -
+ tooltipRect.width / 2;
+ let top = anchorRect.top - containerRect.top;
+ return { left, top };
+ },
+
+ /**
+ * Remove all search indicators. This would be called when switching away from
+ * a search to another preference category.
+ */
+ removeAllSearchIndicators(window, showSubItems) {
+ if (this.searchResultsHighlighted) {
+ this.getFindSelection(window).removeAllRanges();
+ this.searchResultsHighlighted = false;
+ }
+ this.removeAllSearchTooltips();
+ this.removeAllSearchMenuitemIndicators();
+
+ // Make any previously hidden subitems visible again for the next search.
+ if (showSubItems && this.subItems.size) {
+ for (let subItem of this.subItems.keys()) {
+ subItem.classList.remove("visually-hidden");
+ }
+ this.subItems.clear();
+ }
+ },
+
+ /**
+ * Remove all search tooltips.
+ */
+ removeAllSearchTooltips() {
+ for (let anchorNode of this.listSearchTooltips) {
+ anchorNode.parentElement.classList.remove("search-tooltip-parent");
+ if (anchorNode.tooltipNode) {
+ anchorNode.tooltipNode.remove();
+ }
+ anchorNode.tooltipNode = null;
+ }
+ this.listSearchTooltips.clear();
+ },
+
+ /**
+ * Remove all indicators on menuitem.
+ */
+ removeAllSearchMenuitemIndicators() {
+ for (let node of this.listSearchMenuitemIndicators) {
+ node.removeAttribute("indicator");
+ }
+ this.listSearchMenuitemIndicators.clear();
+ },
+};
diff --git a/browser/components/preferences/fxaPairDevice.js b/browser/components/preferences/fxaPairDevice.js
new file mode 100644
index 0000000000..8f5c26734d
--- /dev/null
+++ b/browser/components/preferences/fxaPairDevice.js
@@ -0,0 +1,144 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { FxAccounts } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+const { Weave } = ChromeUtils.importESModule(
+ "resource://services-sync/main.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
+ FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs",
+});
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const QR = require("devtools/shared/qrcode/index");
+
+// This is only for "labor illusion", see
+// https://www.fastcompany.com/3061519/the-ux-secret-that-will-ruin-apps-for-you
+const MIN_PAIRING_LOADING_TIME_MS = 1000;
+
+/**
+ * Communication between FxAccountsPairingFlow and gFxaPairDeviceDialog
+ * is done using an emitter via the following messages:
+ * <- [view:SwitchToWebContent] - Notifies the view to navigate to a specific URL.
+ * <- [view:Error] - Notifies the view something went wrong during the pairing process.
+ * -> [view:Closed] - Notifies the pairing module the view was closed.
+ */
+var gFxaPairDeviceDialog = {
+ init() {
+ this._resetBackgroundQR();
+ // We let the modal show itself before eventually showing a primary-password dialog later.
+ Services.tm.dispatchToMainThread(() => this.startPairingFlow());
+ },
+
+ uninit() {
+ // When the modal closes we want to remove any query params
+ // To prevent refreshes/restores from reopening the dialog
+ const browser = window.docShell.chromeEventHandler;
+ browser.loadURI(Services.io.newURI("about:preferences#sync"), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ this.teardownListeners();
+ this._emitter.emit("view:Closed");
+ },
+
+ async startPairingFlow() {
+ this._resetBackgroundQR();
+ document
+ .getElementById("qrWrapper")
+ .setAttribute("pairing-status", "loading");
+ this._emitter = new EventEmitter();
+ this.setupListeners();
+ try {
+ if (!Weave.Utils.ensureMPUnlocked()) {
+ throw new Error("Master-password locked.");
+ }
+ // To keep consistent with our accounts.firefox.com counterpart
+ // we restyle the parent dialog this is contained in
+ this._styleParentDialog();
+
+ const [, uri] = await Promise.all([
+ new Promise(res => setTimeout(res, MIN_PAIRING_LOADING_TIME_MS)),
+ FxAccountsPairingFlow.start({ emitter: this._emitter }),
+ ]);
+ const imgData = QR.encodeToDataURI(uri, "L");
+ document.getElementById(
+ "qrContainer"
+ ).style.backgroundImage = `url("${imgData.src}")`;
+ document
+ .getElementById("qrWrapper")
+ .setAttribute("pairing-status", "ready");
+ } catch (e) {
+ this.onError(e);
+ }
+ },
+
+ _styleParentDialog() {
+ // Since the dialog title is in the above document, we can't query the
+ // document in this level and need to go up one
+ let dialogParent = window.parent.document;
+
+ // To allow the firefox icon to go over the dialog
+ let dialogBox = dialogParent.querySelector(".dialogBox");
+ dialogBox.style.overflow = "visible";
+ dialogBox.style.borderRadius = "12px";
+
+ let dialogTitle = dialogParent.querySelector(".dialogTitleBar");
+ dialogTitle.style.borderBottom = "none";
+ dialogTitle.classList.add("fxaPairDeviceIcon");
+ },
+
+ _resetBackgroundQR() {
+ // The text we encode doesn't really matter as it is un-scannable (blurry and very transparent).
+ const imgData = QR.encodeToDataURI(
+ "https://accounts.firefox.com/pair",
+ "L"
+ );
+ document.getElementById(
+ "qrContainer"
+ ).style.backgroundImage = `url("${imgData.src}")`;
+ },
+
+ onError(err) {
+ console.error(err);
+ this.teardownListeners();
+ document
+ .getElementById("qrWrapper")
+ .setAttribute("pairing-status", "error");
+ },
+
+ _switchToUrl(url) {
+ const browser = window.docShell.chromeEventHandler;
+ browser.fixupAndLoadURIString(url, {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ });
+ },
+
+ setupListeners() {
+ this._switchToWebContent = (_, url) => this._switchToUrl(url);
+ this._onError = (_, error) => this.onError(error);
+ this._emitter.once("view:SwitchToWebContent", this._switchToWebContent);
+ this._emitter.on("view:Error", this._onError);
+ },
+
+ teardownListeners() {
+ try {
+ this._emitter.off("view:SwitchToWebContent", this._switchToWebContent);
+ this._emitter.off("view:Error", this._onError);
+ } catch (e) {
+ console.warn("Error while tearing down listeners.", e);
+ }
+ },
+};
diff --git a/browser/components/preferences/fxaPairDevice.xhtml b/browser/components/preferences/fxaPairDevice.xhtml
new file mode 100644
index 0000000000..8b66cbe00e
--- /dev/null
+++ b/browser/components/preferences/fxaPairDevice.xhtml
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/fxaPairDevice.css" type="text/css"?>
+
+<window
+ id="fxaPairDeviceDialog"
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gFxaPairDeviceDialog.init();"
+ onunload="gFxaPairDeviceDialog.uninit()"
+ data-l10n-id="fxa-pair-device-dialog-sync2"
+ data-l10n-attrs="style"
+>
+ <dialog id="fxaPairDeviceDialog1" buttons="accept">
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/fxaPairDevice.ftl"
+ />
+ <html:link rel="localization" href="toolkit/branding/accounts.ftl" />
+ </linkset>
+ <script src="chrome://browser/content/preferences/fxaPairDevice.js" />
+
+ <description id="pairTitle" data-l10n-id="fxa-qrcode-pair-title">
+ </description>
+ <vbox id="qrCodeDisplay">
+ <description class="pairHeading" data-l10n-id="fxa-qrcode-pair-step1">
+ </description>
+ <description
+ class="pairHeading"
+ data-l10n-id="fxa-qrcode-pair-step2-signin"
+ >
+ <html:img
+ src="chrome://browser/skin/preferences/ios-menu.svg"
+ data-l10n-name="ios-menu-icon"
+ class="menu-icon"
+ />
+ <html:img
+ src="chrome://browser/skin/preferences/android-menu.svg"
+ data-l10n-name="android-menu-icon"
+ class="menu-icon"
+ />
+ </description>
+ <description
+ class="pairHeading"
+ data-l10n-id="fxa-qrcode-pair-step3"
+ ></description>
+ <vbox>
+ <vbox align="center" id="qrWrapper" pairing-status="loading">
+ <box id="qrContainer"></box>
+ <box id="qrSpinner"></box>
+ <vbox id="qrError" onclick="gFxaPairDeviceDialog.startPairingFlow();">
+ <image id="refresh-qr" />
+ <label
+ class="qr-error-text"
+ data-l10n-id="fxa-qrcode-error-title"
+ ></label>
+ <label
+ class="qr-error-text"
+ data-l10n-id="fxa-qrcode-error-body"
+ ></label>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml
new file mode 100644
index 0000000000..9e6121e28d
--- /dev/null
+++ b/browser/components/preferences/home.inc.xhtml
@@ -0,0 +1,92 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!-- Home panel -->
+
+<script src="chrome://browser/content/preferences/home.js"/>
+<html:template id="template-paneHome">
+<hbox id="firefoxHomeCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneHome">
+ <html:h1 style="flex: 1;" data-l10n-id="pane-home-title"/>
+ <button id="restoreDefaultHomePageBtn"
+ is="highlightable-button"
+ class="homepage-button check-home-page-controlled"
+ data-preference-related="browser.startup.homepage"
+ data-l10n-id="home-restore-defaults"
+ preference="pref.browser.homepage.disable_button.restore_default"/>
+</hbox>
+
+<groupbox id="homepageGroup"
+ data-category="paneHome"
+ hidden="true">
+ <label><html:h2 data-l10n-id="home-new-windows-tabs-header"/></label>
+ <description data-l10n-id="home-new-windows-tabs-description2" />
+
+ <hbox id="homepageAndNewWindowsOption" align="center" data-subcategory="homeOverride">
+ <label control="homeMode" data-l10n-id="home-homepage-mode-label" flex="1" />
+
+ <vbox flex="1">
+ <menulist id="homeMode"
+ class="check-home-page-controlled"
+ data-preference-related="browser.startup.homepage">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="home-mode-choice-default-fx" />
+ <menuitem value="2" data-l10n-id="home-mode-choice-custom" />
+ <menuitem value="1" data-l10n-id="home-mode-choice-blank" />
+ </menupopup>
+ </menulist>
+
+ <vbox id="customSettings" hidden="true">
+ <box role="combobox">
+ <html:input id="homePageUrl"
+ type="text"
+ is="autocomplete-input"
+ class="uri-element check-home-page-controlled"
+ style="flex: 1;"
+ data-preference-related="browser.startup.homepage"
+ data-l10n-id="home-homepage-custom-url"
+ autocompletepopup="homePageUrlAutocomplete" />
+ <popupset>
+ <panel id="homePageUrlAutocomplete"
+ is="autocomplete-richlistbox-popup"
+ type="autocomplete-richlistbox"
+ noautofocus="true"/>
+ </popupset>
+ </box>
+ <hbox class="homepage-buttons">
+ <button id="useCurrentBtn"
+ is="highlightable-button"
+ class="homepage-button check-home-page-controlled"
+ data-l10n-id="use-current-pages"
+ data-l10n-args='{"tabCount": 0}'
+ disabled="true"
+ preference="pref.browser.homepage.disable_button.current_page"/>
+ <button id="useBookmarkBtn"
+ is="highlightable-button"
+ class="homepage-button check-home-page-controlled"
+ data-l10n-id="choose-bookmark"
+ preference="pref.browser.homepage.disable_button.bookmark_page"
+ search-l10n-ids="select-bookmark-window2.title, select-bookmark-desc"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </hbox>
+ <hbox id="newTabsOption" data-subcategory="newtabOverride" align="center">
+ <label control="newTabMode" data-l10n-id="home-newtabs-mode-label" flex="1" />
+
+ <vbox flex="1">
+ <!-- This can be set to an extension value which is managed outside of
+ Preferences so we need to handle setting the pref manually.-->
+ <menulist id="newTabMode" flex="1" data-preference-related="browser.newtabpage.enabled">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="home-mode-choice-default-fx" />
+ <menuitem value="1" data-l10n-id="home-mode-choice-blank" />
+ </menupopup>
+ </menulist>
+ </vbox>
+ </hbox>
+</groupbox>
+</html:template>
diff --git a/browser/components/preferences/home.js b/browser/components/preferences/home.js
new file mode 100644
index 0000000000..6aa72b84b8
--- /dev/null
+++ b/browser/components/preferences/home.js
@@ -0,0 +1,694 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+/* import-globals-from main.js */
+
+// HOME PAGE
+
+/*
+ * Preferences:
+ *
+ * browser.startup.homepage
+ * - the user's home page, as a string; if the home page is a set of tabs,
+ * this will be those URLs separated by the pipe character "|"
+ * browser.newtabpage.enabled
+ * - determines that is shown on the user's new tab page.
+ * true = Activity Stream is shown,
+ * false = about:blank is shown
+ */
+
+Preferences.addAll([
+ { id: "browser.startup.homepage", type: "wstring" },
+ { id: "pref.browser.homepage.disable_button.current_page", type: "bool" },
+ { id: "pref.browser.homepage.disable_button.bookmark_page", type: "bool" },
+ { id: "pref.browser.homepage.disable_button.restore_default", type: "bool" },
+ { id: "browser.newtabpage.enabled", type: "bool" },
+]);
+
+const HOMEPAGE_OVERRIDE_KEY = "homepage_override";
+const URL_OVERRIDES_TYPE = "url_overrides";
+const NEW_TAB_KEY = "newTabURL";
+
+const BLANK_HOMEPAGE_URL = "chrome://browser/content/blanktab.html";
+
+var gHomePane = {
+ HOME_MODE_FIREFOX_HOME: "0",
+ HOME_MODE_BLANK: "1",
+ HOME_MODE_CUSTOM: "2",
+ HOMEPAGE_PREF: "browser.startup.homepage",
+ NEWTAB_ENABLED_PREF: "browser.newtabpage.enabled",
+ ACTIVITY_STREAM_PREF_BRANCH: "browser.newtabpage.activity-stream.",
+
+ get homePanePrefs() {
+ return Preferences.getAll().filter(pref =>
+ pref.id.includes(this.ACTIVITY_STREAM_PREF_BRANCH)
+ );
+ },
+
+ get isPocketNewtabEnabled() {
+ const value = Services.prefs.getStringPref(
+ "browser.newtabpage.activity-stream.discoverystream.config",
+ ""
+ );
+ if (value) {
+ try {
+ return JSON.parse(value).enabled;
+ } catch (e) {
+ console.error("Failed to parse Discovery Stream pref.");
+ }
+ }
+
+ return false;
+ },
+
+ async syncToNewTabPref() {
+ let menulist = document.getElementById("newTabMode");
+
+ if (["0", "1"].includes(menulist.value)) {
+ let newtabEnabledPref = Services.prefs.getBoolPref(
+ this.NEWTAB_ENABLED_PREF,
+ true
+ );
+ let newValue = menulist.value !== this.HOME_MODE_BLANK;
+ // Only set this if the pref has changed, otherwise the pref change will trigger other listeners to repeat.
+ if (newtabEnabledPref !== newValue) {
+ Services.prefs.setBoolPref(this.NEWTAB_ENABLED_PREF, newValue);
+ }
+ let selectedAddon = ExtensionSettingsStore.getSetting(
+ URL_OVERRIDES_TYPE,
+ NEW_TAB_KEY
+ );
+ if (selectedAddon) {
+ ExtensionSettingsStore.select(null, URL_OVERRIDES_TYPE, NEW_TAB_KEY);
+ }
+ } else {
+ let addon = await AddonManager.getAddonByID(menulist.value);
+ if (addon && addon.isActive) {
+ ExtensionSettingsStore.select(
+ addon.id,
+ URL_OVERRIDES_TYPE,
+ NEW_TAB_KEY
+ );
+ }
+ }
+ },
+
+ async syncFromNewTabPref() {
+ let menulist = document.getElementById("newTabMode");
+
+ // If the new tab url was changed to about:blank or about:newtab
+ if (
+ AboutNewTab.newTabURL === "about:newtab" ||
+ AboutNewTab.newTabURL === "about:blank" ||
+ AboutNewTab.newTabURL === BLANK_HOMEPAGE_URL
+ ) {
+ let newtabEnabledPref = Services.prefs.getBoolPref(
+ this.NEWTAB_ENABLED_PREF,
+ true
+ );
+ let newValue = newtabEnabledPref
+ ? this.HOME_MODE_FIREFOX_HOME
+ : this.HOME_MODE_BLANK;
+ if (newValue !== menulist.value) {
+ menulist.value = newValue;
+ }
+ menulist.disabled = Preferences.get(this.NEWTAB_ENABLED_PREF).locked;
+ // If change was triggered by installing an addon we need to update
+ // the value of the menulist to be that addon.
+ } else {
+ let selectedAddon = ExtensionSettingsStore.getSetting(
+ URL_OVERRIDES_TYPE,
+ NEW_TAB_KEY
+ );
+ if (selectedAddon && menulist.value !== selectedAddon.id) {
+ menulist.value = selectedAddon.id;
+ }
+ }
+ },
+
+ /**
+ * _updateMenuInterface: adds items to or removes them from the menulists
+ * @param {string} selectId Optional Id of the menulist to add or remove items from.
+ * If not included this will update both home and newtab menus.
+ */
+ async _updateMenuInterface(selectId) {
+ let selects;
+ if (selectId) {
+ selects = [document.getElementById(selectId)];
+ } else {
+ let newTabSelect = document.getElementById("newTabMode");
+ let homeSelect = document.getElementById("homeMode");
+ selects = [homeSelect, newTabSelect];
+ }
+
+ for (let select of selects) {
+ // Remove addons from the menu popup which are no longer installed, or disabled.
+ // let menuOptions = select.menupopup.childNodes;
+ let menuOptions = Array.from(select.menupopup.childNodes);
+
+ for (let option of menuOptions) {
+ // If the value is not a number, assume it is an addon ID
+ if (!/^\d+$/.test(option.value)) {
+ let addon = await AddonManager.getAddonByID(option.value);
+ if (option && (!addon || !addon.isActive)) {
+ option.remove();
+ }
+ }
+ }
+
+ let extensionOptions;
+ if (select.id === "homeMode") {
+ extensionOptions = await ExtensionSettingsStore.getAllSettings(
+ PREF_SETTING_TYPE,
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ } else {
+ extensionOptions = await ExtensionSettingsStore.getAllSettings(
+ URL_OVERRIDES_TYPE,
+ NEW_TAB_KEY
+ );
+ }
+ let addons = await AddonManager.getAddonsByIDs(
+ extensionOptions.map(a => a.id)
+ );
+
+ // Add addon options to the menu popups
+ let menupopup = select.querySelector("menupopup");
+ for (let addon of addons) {
+ if (!addon || !addon.id || !addon.isActive) {
+ continue;
+ }
+ let currentOption = select.querySelector(
+ `[value="${CSS.escape(addon.id)}"]`
+ );
+ if (!currentOption) {
+ let option = document.createXULElement("menuitem");
+ option.classList.add("addon-with-favicon");
+ option.value = addon.id;
+ option.label = addon.name;
+ menupopup.append(option);
+ option.querySelector("image").src = addon.iconURL;
+ }
+ let setting = extensionOptions.find(o => o.id == addon.id);
+ if (
+ (select.id === "homeMode" && setting.value == HomePage.get()) ||
+ (select.id === "newTabMode" && setting.value == AboutNewTab.newTabURL)
+ ) {
+ select.value = addon.id;
+ }
+ }
+ }
+ },
+
+ /**
+ * watchNewTab: Listen for changes to the new tab url and enable/disable appropriate
+ * areas of the UI.
+ */
+ watchNewTab() {
+ let newTabObserver = () => {
+ this.syncFromNewTabPref();
+ this._updateMenuInterface("newTabMode");
+ };
+ Services.obs.addObserver(newTabObserver, "newtab-url-changed");
+ window.addEventListener("unload", () => {
+ Services.obs.removeObserver(newTabObserver, "newtab-url-changed");
+ });
+ },
+
+ /**
+ * watchHomePrefChange: Listen for preferences changes on the Home Tab in order to
+ * show the appropriate home menu selection.
+ */
+ watchHomePrefChange() {
+ const homePrefObserver = (subject, topic, data) => {
+ // only update this UI if it is exactly the HOMEPAGE_PREF, not other prefs with the same root.
+ if (data && data != this.HOMEPAGE_PREF) {
+ return;
+ }
+ this._updateUseCurrentButton();
+ this._renderCustomSettings();
+ this._handleHomePageOverrides();
+ this._updateMenuInterface("homeMode");
+ };
+
+ Services.prefs.addObserver(this.HOMEPAGE_PREF, homePrefObserver);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver(this.HOMEPAGE_PREF, homePrefObserver);
+ });
+ },
+
+ /**
+ * Listen extension changes on the New Tab and Home Tab
+ * in order to update the UI and show or hide the Restore Defaults button.
+ */
+ watchExtensionPrefChange() {
+ const extensionSettingChanged = (evt, setting) => {
+ if (setting.key == "homepage_override" && setting.type == "prefs") {
+ this._updateMenuInterface("homeMode");
+ } else if (
+ setting.key == "newTabURL" &&
+ setting.type == "url_overrides"
+ ) {
+ this._updateMenuInterface("newTabMode");
+ }
+ };
+
+ Management.on("extension-setting-changed", extensionSettingChanged);
+ window.addEventListener("unload", () => {
+ Management.off("extension-setting-changed", extensionSettingChanged);
+ });
+ },
+
+ /**
+ * Listen for all preferences changes on the Home Tab in order to show or
+ * hide the Restore Defaults button.
+ */
+ watchHomeTabPrefChange() {
+ const observer = () => this.toggleRestoreDefaultsBtn();
+ Services.prefs.addObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer);
+ Services.prefs.addObserver(this.HOMEPAGE_PREF, observer);
+ Services.prefs.addObserver(this.NEWTAB_ENABLED_PREF, observer);
+
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer);
+ Services.prefs.removeObserver(this.HOMEPAGE_PREF, observer);
+ Services.prefs.removeObserver(this.NEWTAB_ENABLED_PREF, observer);
+ });
+ },
+
+ /**
+ * _renderCustomSettings: Hides or shows the UI for setting a custom
+ * homepage URL
+ * @param {obj} options
+ * @param {bool} options.shouldShow Should the custom UI be shown?
+ * @param {bool} options.isControlled Is an extension controlling the home page?
+ */
+ _renderCustomSettings(options = {}) {
+ let { shouldShow, isControlled } = options;
+ const customSettingsContainerEl = document.getElementById("customSettings");
+ const customUrlEl = document.getElementById("homePageUrl");
+ const homePage = HomePage.get();
+ const isHomePageCustom =
+ (!this._isHomePageDefaultValue() &&
+ !this.isHomePageBlank() &&
+ !isControlled) ||
+ homePage.locked;
+
+ if (typeof shouldShow === "undefined") {
+ shouldShow = isHomePageCustom;
+ }
+ customSettingsContainerEl.hidden = !shouldShow;
+
+ // We can't use isHomePageDefaultValue and isHomePageBlank here because we want to disregard the blank
+ // possibility triggered by the browser.startup.page being 0.
+ // We also skip when HomePage is locked because it might be locked to a default that isn't "about:home"
+ // (and it makes existing tests happy).
+ let newValue;
+ if (
+ this._isBlankPage(homePage) ||
+ (HomePage.isDefault && !HomePage.locked)
+ ) {
+ newValue = "";
+ } else {
+ newValue = homePage;
+ }
+ if (customUrlEl.value !== newValue) {
+ customUrlEl.value = newValue;
+ }
+ },
+
+ /**
+ * _isHomePageDefaultValue
+ * @returns {bool} Is the homepage set to the default pref value?
+ */
+ _isHomePageDefaultValue() {
+ const startupPref = Preferences.get("browser.startup.page");
+ return (
+ startupPref.value !== gMainPane.STARTUP_PREF_BLANK && HomePage.isDefault
+ );
+ },
+
+ /**
+ * isHomePageBlank
+ * @returns {bool} Is the homepage set to about:blank?
+ */
+ isHomePageBlank() {
+ const startupPref = Preferences.get("browser.startup.page");
+ return (
+ ["about:blank", BLANK_HOMEPAGE_URL, ""].includes(HomePage.get()) ||
+ startupPref.value === gMainPane.STARTUP_PREF_BLANK
+ );
+ },
+
+ /**
+ * _isTabAboutPreferences: Is a given tab set to about:preferences?
+ * @param {Element} aTab A tab element
+ * @returns {bool} Is the linkedBrowser of aElement set to about:preferences?
+ */
+ _isTabAboutPreferences(aTab) {
+ return aTab.linkedBrowser.currentURI.spec.startsWith("about:preferences");
+ },
+
+ /**
+ * _getTabsForHomePage
+ * @returns {Array} An array of current tabs
+ */
+ _getTabsForHomePage() {
+ let tabs = [];
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // We should only include visible & non-pinned tabs
+ if (
+ win &&
+ win.document.documentElement.getAttribute("windowtype") ===
+ "navigator:browser"
+ ) {
+ tabs = win.gBrowser.visibleTabs.slice(win.gBrowser._numPinnedTabs);
+ tabs = tabs.filter(tab => !this._isTabAboutPreferences(tab));
+ // XXX: Bug 1441637 - Fix tabbrowser to report tab.closing before it blurs it
+ tabs = tabs.filter(tab => !tab.closing);
+ }
+
+ return tabs;
+ },
+
+ _renderHomepageMode(controllingExtension) {
+ const isDefault = this._isHomePageDefaultValue();
+ const isBlank = this.isHomePageBlank();
+ const el = document.getElementById("homeMode");
+ let newValue;
+
+ if (controllingExtension && controllingExtension.id) {
+ newValue = controllingExtension.id;
+ } else if (isDefault) {
+ newValue = this.HOME_MODE_FIREFOX_HOME;
+ } else if (isBlank) {
+ newValue = this.HOME_MODE_BLANK;
+ } else {
+ newValue = this.HOME_MODE_CUSTOM;
+ }
+ if (el.value !== newValue) {
+ el.value = newValue;
+ }
+ },
+
+ _setInputDisabledStates(isControlled) {
+ let tabCount = this._getTabsForHomePage().length;
+
+ // Disable or enable the inputs based on if this is controlled by an extension.
+ document
+ .querySelectorAll(".check-home-page-controlled")
+ .forEach(element => {
+ let isDisabled;
+ let pref =
+ element.getAttribute("preference") ||
+ element.getAttribute("data-preference-related");
+ if (!pref) {
+ throw new Error(
+ `Element with id ${element.id} did not have preference or data-preference-related attribute defined.`
+ );
+ }
+
+ if (pref === this.HOMEPAGE_PREF) {
+ isDisabled = HomePage.locked;
+ } else {
+ isDisabled = Preferences.get(pref).locked || isControlled;
+ }
+
+ if (pref === "pref.browser.disable_button.current_page") {
+ // Special case for current_page to disable it if tabCount is 0
+ isDisabled = isDisabled || tabCount < 1;
+ }
+
+ element.disabled = isDisabled;
+ });
+ },
+
+ async _handleHomePageOverrides() {
+ let controllingExtension;
+ if (HomePage.locked) {
+ // Disable inputs if they are locked.
+ this._renderCustomSettings();
+ this._setInputDisabledStates(false);
+ } else {
+ if (HomePage.get().startsWith("moz-extension:")) {
+ controllingExtension = await getControllingExtension(
+ PREF_SETTING_TYPE,
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ }
+ this._setInputDisabledStates();
+ this._renderCustomSettings({
+ isControlled: !!controllingExtension,
+ });
+ }
+ this._renderHomepageMode(controllingExtension);
+ },
+
+ onMenuChange(event) {
+ const { value } = event.target;
+ const startupPref = Preferences.get("browser.startup.page");
+ let selectedAddon = ExtensionSettingsStore.getSetting(
+ PREF_SETTING_TYPE,
+ HOMEPAGE_OVERRIDE_KEY
+ );
+
+ switch (value) {
+ case this.HOME_MODE_FIREFOX_HOME:
+ if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
+ startupPref.value = gMainPane.STARTUP_PREF_HOMEPAGE;
+ }
+ if (!HomePage.isDefault) {
+ HomePage.reset();
+ } else {
+ this._renderCustomSettings({ shouldShow: false });
+ }
+ if (selectedAddon) {
+ ExtensionSettingsStore.select(
+ null,
+ PREF_SETTING_TYPE,
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ }
+ break;
+ case this.HOME_MODE_BLANK:
+ if (!this._isBlankPage(HomePage.get())) {
+ HomePage.safeSet(BLANK_HOMEPAGE_URL);
+ } else {
+ this._renderCustomSettings({ shouldShow: false });
+ }
+ if (selectedAddon) {
+ ExtensionSettingsStore.select(
+ null,
+ PREF_SETTING_TYPE,
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ }
+ break;
+ case this.HOME_MODE_CUSTOM:
+ if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
+ Services.prefs.clearUserPref(startupPref.id);
+ }
+ if (HomePage.getDefault() != HomePage.getOriginalDefault()) {
+ HomePage.clear();
+ }
+ this._renderCustomSettings({ shouldShow: true });
+ if (selectedAddon) {
+ ExtensionSettingsStore.select(
+ null,
+ PREF_SETTING_TYPE,
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ }
+ break;
+ // extensions will have a variety of values as their ID, so treat it as default
+ default:
+ AddonManager.getAddonByID(value).then(addon => {
+ if (addon && addon.isActive) {
+ ExtensionPreferencesManager.selectSetting(
+ addon.id,
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ }
+ this._renderCustomSettings({ shouldShow: false });
+ });
+ }
+ },
+
+ /**
+ * Switches the "Use Current Page" button between its singular and plural
+ * forms.
+ */
+ async _updateUseCurrentButton() {
+ let useCurrent = document.getElementById("useCurrentBtn");
+ let tabs = this._getTabsForHomePage();
+ const tabCount = tabs.length;
+ document.l10n.setAttributes(useCurrent, "use-current-pages", { tabCount });
+
+ // If the homepage is controlled by an extension then you can't use this.
+ if (
+ await getControllingExtensionInfo(
+ PREF_SETTING_TYPE,
+ HOMEPAGE_OVERRIDE_KEY
+ )
+ ) {
+ return;
+ }
+
+ // In this case, the button's disabled state is set by preferences.xml.
+ let prefName = "pref.browser.homepage.disable_button.current_page";
+ if (Preferences.get(prefName).locked) {
+ return;
+ }
+
+ useCurrent.disabled = tabCount < 1;
+ },
+
+ /**
+ * Sets the home page to the URL(s) of any currently opened tab(s),
+ * updating about:preferences#home UI to reflect this.
+ */
+ setHomePageToCurrent() {
+ let tabs = this._getTabsForHomePage();
+ function getTabURI(t) {
+ return t.linkedBrowser.currentURI.spec;
+ }
+
+ // FIXME Bug 244192: using dangerous "|" joiner!
+ if (tabs.length) {
+ HomePage.set(tabs.map(getTabURI).join("|")).catch(console.error);
+ }
+ },
+
+ _setHomePageToBookmarkClosed(rv, aEvent) {
+ if (aEvent.detail.button != "accept") {
+ return;
+ }
+ if (rv.urls && rv.names) {
+ // XXX still using dangerous "|" joiner!
+ HomePage.set(rv.urls.join("|")).catch(console.error);
+ }
+ },
+
+ /**
+ * Displays a dialog in which the user can select a bookmark to use as home
+ * page. If the user selects a bookmark, that bookmark's name is displayed in
+ * UI and the bookmark's address is stored to the home page preference.
+ */
+ setHomePageToBookmark() {
+ const rv = { urls: null, names: null };
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml",
+ {
+ features: "resizable=yes, modal=yes",
+ closingCallback: this._setHomePageToBookmarkClosed.bind(this, rv),
+ },
+ rv
+ );
+ },
+
+ restoreDefaultHomePage() {
+ HomePage.reset();
+ this._handleHomePageOverrides();
+ Services.prefs.clearUserPref(this.NEWTAB_ENABLED_PREF);
+ AboutNewTab.resetNewTabURL();
+ },
+
+ onCustomHomePageChange(event) {
+ const value = event.target.value || HomePage.getDefault();
+ HomePage.set(value).catch(console.error);
+ },
+
+ /**
+ * Check all Home Tab preferences for user set values.
+ */
+ _changedHomeTabDefaultPrefs() {
+ // If Discovery Stream is enabled Firefox Home Content preference options are hidden
+ const homeContentChanged =
+ !this.isPocketNewtabEnabled &&
+ this.homePanePrefs.some(pref => pref.hasUserValue);
+ const newtabPref = Preferences.get(this.NEWTAB_ENABLED_PREF);
+ const extensionControlled = Preferences.get(
+ "browser.startup.homepage_override.extensionControlled"
+ );
+
+ return (
+ homeContentChanged ||
+ HomePage.overridden ||
+ newtabPref.hasUserValue ||
+ AboutNewTab.newTabURLOverridden ||
+ extensionControlled
+ );
+ },
+
+ _isBlankPage(url) {
+ return url == "about:blank" || url == BLANK_HOMEPAGE_URL;
+ },
+
+ /**
+ * Show the Restore Defaults button if any preference on the Home tab was
+ * changed, or hide it otherwise.
+ */
+ toggleRestoreDefaultsBtn() {
+ const btn = document.getElementById("restoreDefaultHomePageBtn");
+ const prefChanged = this._changedHomeTabDefaultPrefs();
+ if (prefChanged) {
+ btn.style.removeProperty("visibility");
+ } else {
+ btn.style.visibility = "hidden";
+ }
+ },
+
+ /**
+ * Set all prefs on the Home tab back to their default values.
+ */
+ restoreDefaultPrefsForHome() {
+ this.restoreDefaultHomePage();
+ // If Discovery Stream is enabled Firefox Home Content preference options are hidden
+ if (!this.isPocketNewtabEnabled) {
+ this.homePanePrefs.forEach(pref => Services.prefs.clearUserPref(pref.id));
+ }
+ },
+
+ init() {
+ // Event Listeners
+ document
+ .getElementById("homePageUrl")
+ .addEventListener("change", this.onCustomHomePageChange.bind(this));
+ document
+ .getElementById("useCurrentBtn")
+ .addEventListener("command", this.setHomePageToCurrent.bind(this));
+ document
+ .getElementById("useBookmarkBtn")
+ .addEventListener("command", this.setHomePageToBookmark.bind(this));
+ document
+ .getElementById("restoreDefaultHomePageBtn")
+ .addEventListener("command", this.restoreDefaultPrefsForHome.bind(this));
+
+ // Setup the add-on options for the new tab section before registering the
+ // listener.
+ this._updateMenuInterface();
+ document
+ .getElementById("newTabMode")
+ .addEventListener("command", this.syncToNewTabPref.bind(this));
+ document
+ .getElementById("homeMode")
+ .addEventListener("command", this.onMenuChange.bind(this));
+
+ this._updateUseCurrentButton();
+ this._handleHomePageOverrides();
+ this.syncFromNewTabPref();
+ window.addEventListener("focus", this._updateUseCurrentButton.bind(this));
+
+ // Extension/override-related events
+ this.watchNewTab();
+ this.watchHomePrefChange();
+ this.watchExtensionPrefChange();
+ this.watchHomeTabPrefChange();
+ // Notify observers that the UI is now ready
+ Services.obs.notifyObservers(window, "home-pane-loaded");
+ },
+};
diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn
new file mode 100644
index 0000000000..2131a15cee
--- /dev/null
+++ b/browser/components/preferences/jar.mn
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+browser.jar:
+ content/browser/preferences/preferences.js
+ content/browser/preferences/extensionControlled.js
+* content/browser/preferences/preferences.xhtml
+
+ content/browser/preferences/main.js
+ content/browser/preferences/home.js
+ content/browser/preferences/search.js
+ content/browser/preferences/privacy.js
+ content/browser/preferences/containers.js
+ content/browser/preferences/sync.js
+ content/browser/preferences/experimental.js
+ content/browser/preferences/moreFromMozilla.js
+ content/browser/preferences/fxaPairDevice.xhtml
+ content/browser/preferences/fxaPairDevice.js
+ content/browser/preferences/findInPage.js
+ content/browser/preferences/more-from-mozilla-qr-code-simple.svg
+ content/browser/preferences/more-from-mozilla-qr-code-simple-cn.svg
+ content/browser/preferences/web-appearance-dark.svg
+ content/browser/preferences/web-appearance-light.svg
diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml
new file mode 100644
index 0000000000..8f87b2f5d9
--- /dev/null
+++ b/browser/components/preferences/main.inc.xhtml
@@ -0,0 +1,837 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!-- General panel -->
+
+<script src="chrome://browser/content/preferences/main.js"/>
+
+#ifdef MOZ_UPDATER
+ <script src="chrome://browser/content/aboutDialog-appUpdater.js"/>
+#endif
+
+<script src="chrome://mozapps/content/preferences/fontbuilder.js"/>
+
+<html:template id="template-paneGeneral">
+<hbox id="generalCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="pane-general-title"/>
+</hbox>
+
+<!-- Startup -->
+<groupbox id="startupGroup"
+ data-category="paneGeneral"
+ hidden="true">
+ <label><html:h2 data-l10n-id="startup-header"/></label>
+
+ <vbox id="startupPageBox">
+ <checkbox id="browserRestoreSession"
+ data-l10n-id="startup-restore-windows-and-tabs"/>
+ </vbox>
+
+#ifdef HAVE_SHELL_SERVICE
+ <vbox id="defaultBrowserBox">
+ <checkbox id="alwaysCheckDefault" preference="browser.shell.checkDefaultBrowser"
+ disabled="true"
+ data-l10n-id="always-check-default"/>
+ <stack id="setDefaultPane">
+ <hbox id="isNotDefaultPane" align="center" class="indent">
+ <label class="face-sad" id="isNotDefaultLabel" flex="1" data-l10n-id="is-not-default"/>
+ <button id="setDefaultButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="set-as-my-default-browser"
+ preference="pref.general.disable_button.default_browser"/>
+ </hbox>
+ <hbox id="isDefaultPane" align="center" class="indent">
+ <label class="face-smile" id="isDefaultLabel" flex="1" data-l10n-id="is-default"/>
+ </hbox>
+ </stack>
+ </vbox>
+#endif
+
+</groupbox>
+
+<!-- Data migration -->
+<groupbox id="dataMigrationGroup" data-category="paneGeneral" hidden="true">
+ <label><html:h2 data-l10n-id="preferences-data-migration-header"/></label>
+
+ <hbox id="dataMigration" flex="1">
+ <description flex="1" control="data-migration" data-l10n-id="preferences-data-migration-description"/>
+ <button id="data-migration"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="preferences-data-migration-button"/>
+ </hbox>
+</groupbox>
+
+<!-- Tab preferences -->
+<groupbox data-category="paneGeneral"
+ hidden="true">
+ <label><html:h2 data-l10n-id="tabs-group-header"/></label>
+
+ <checkbox id="ctrlTabRecentlyUsedOrder" data-l10n-id="ctrl-tab-recently-used-order"
+ preference="browser.ctrlTab.sortByRecentlyUsed"/>
+
+ <checkbox id="linkTargeting" data-l10n-id="open-new-link-as-tabs"
+ preference="browser.link.open_newwindow"/>
+
+ <checkbox id="warnOpenMany" data-l10n-id="warn-on-open-many-tabs"
+ preference="browser.tabs.warnOnOpen"/>
+
+ <checkbox id="switchToNewTabs" data-l10n-id="switch-to-new-tabs"
+ preference="browser.tabs.loadInBackground"/>
+
+ <checkbox id="warnCloseMultiple" data-l10n-id="confirm-on-close-multiple-tabs"
+ preference="browser.tabs.warnOnClose"/>
+
+#ifndef XP_WIN
+ <checkbox id="warnOnQuitKey" preference="browser.warnOnQuitShortcut"/>
+#endif
+
+#ifdef XP_WIN
+ <checkbox id="showTabsInTaskbar" data-l10n-id="show-tabs-in-taskbar"
+ preference="browser.taskbar.previews.enable"/>
+#endif
+
+ <vbox id="browserContainersbox" hidden="true">
+ <hbox id="browserContainersExtensionContent"
+ align="center" class="extension-controlled info-box-container">
+ <hbox flex="1">
+ <description control="disableContainersExtension" class="description-with-side-element" flex="1" />
+ </hbox>
+ <button id="disableContainersExtension"
+ is="highlightable-button"
+ class="extension-controlled-button accessory-button"
+ data-l10n-id="disable-extension" />
+ </hbox>
+ <hbox align="center">
+ <checkbox id="browserContainersCheckbox"
+ class="tail-with-learn-more"
+ data-l10n-id="browser-containers-enabled"
+ preference="privacy.userContext.enabled"/>
+ <html:a
+ is="moz-support-link"
+ support-page="containers"
+ data-l10n-id="browser-containers-learn-more"
+ />
+ <spacer flex="1"/>
+ <button id="browserContainersSettings"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="browser-containers-settings"
+ search-l10n-ids="containers-add-button.label,
+ containers-settings-button.label,
+ containers-remove-button.label"
+ />
+ </hbox>
+ </vbox>
+</groupbox>
+
+<hbox id="languageAndAppearanceCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="language-and-appearance-header"/>
+</hbox>
+
+<!-- Website appearance -->
+<groupbox id="webAppearanceGroup" data-category="paneGeneral" hidden="true">
+ <html:h2 data-l10n-id="preferences-web-appearance-header"/>
+ <html:div id="webAppearanceSettings">
+ <description data-l10n-id="preferences-web-appearance-description"/>
+ <html:div id="web-appearance-override-warning" class="info-box-container">
+ <html:div class="info-icon-container">
+ <html:img class="info-icon"/>
+ </html:div>
+ <description data-l10n-id="preferences-web-appearance-override-warning">
+ <html:a class="text-link" data-l10n-name="colors-link" id="web-appearance-manage-colors-link" href="#"/>
+ </description>
+ </html:div>
+ <form xmlns="http://www.w3.org/1999/xhtml" id="web-appearance-chooser" autocomplete="off">
+ <label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-auto">
+ <div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div>
+ <div class="web-appearance-choice-footer">
+ <input type="radio" name="web-appearance" value="auto" data-l10n-id="preferences-web-appearance-choice-input-auto"
+ /><span data-l10n-id="preferences-web-appearance-choice-auto" />
+ </div>
+ </label>
+ <label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-light">
+ <div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div>
+ <div class="web-appearance-choice-footer">
+ <input type="radio" name="web-appearance" value="light" data-l10n-id="preferences-web-appearance-choice-input-light"
+ /><span data-l10n-id="preferences-web-appearance-choice-light" />
+ </div>
+ </label>
+ <label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-dark">
+ <div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div>
+ <div class="web-appearance-choice-footer">
+ <input type="radio" name="web-appearance" value="dark" data-l10n-id="preferences-web-appearance-choice-input-dark"
+ /><span data-l10n-id="preferences-web-appearance-choice-dark" />
+ </div>
+ </label>
+ </form>
+ <html:div data-l10n-id="preferences-web-appearance-footer">
+ <html:a id="web-appearance-manage-themes-link" class="text-link" data-l10n-name="themes-link" href="about:addons" target="_blank" />
+ </html:div>
+ </html:div>
+</groupbox>
+
+<!-- Colors -->
+<groupbox id="colorsGroup" data-category="paneGeneral" hidden="true">
+ <label><html:h2 data-l10n-id="preferences-colors-header"/></label>
+
+ <hbox id="colorsSettings" align="center" flex="1">
+ <description flex="1" control="colors" data-l10n-id="preferences-colors-description"/>
+ <button id="colors"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="preferences-colors-manage-button"
+ search-l10n-ids="
+ colors-page-override,
+ colors-page-override-option-always.label,
+ colors-page-override-option-auto.label,
+ colors-page-override-option-never.label,
+ colors-text-and-background,
+ colors-text-header,
+ colors-background,
+ colors-use-system,
+ colors-underline-links,
+ colors-links-header,
+ colors-unvisited-links,
+ colors-visited-links
+ "/>
+ </hbox>
+</groupbox>
+
+<!-- Fonts -->
+<groupbox id="fontsGroup" data-category="paneGeneral" hidden="true">
+ <label><html:h2 data-l10n-id="preferences-fonts-header"/></label>
+
+ <hbox id="fontSettings">
+ <hbox align="center" flex="1">
+ <label control="defaultFont" data-l10n-id="default-font"/>
+ <menulist id="defaultFont" delayprefsave="true"/>
+ <label id="defaultFontSizeLabel" control="defaultFontSize" data-l10n-id="default-font-size"></label>
+ <menulist id="defaultFontSize" delayprefsave="true">
+ <menupopup>
+ <menuitem value="9" label="9"/>
+ <menuitem value="10" label="10"/>
+ <menuitem value="11" label="11"/>
+ <menuitem value="12" label="12"/>
+ <menuitem value="13" label="13"/>
+ <menuitem value="14" label="14"/>
+ <menuitem value="15" label="15"/>
+ <menuitem value="16" label="16"/>
+ <menuitem value="17" label="17"/>
+ <menuitem value="18" label="18"/>
+ <menuitem value="20" label="20"/>
+ <menuitem value="22" label="22"/>
+ <menuitem value="24" label="24"/>
+ <menuitem value="26" label="26"/>
+ <menuitem value="28" label="28"/>
+ <menuitem value="30" label="30"/>
+ <menuitem value="32" label="32"/>
+ <menuitem value="34" label="34"/>
+ <menuitem value="36" label="36"/>
+ <menuitem value="40" label="40"/>
+ <menuitem value="44" label="44"/>
+ <menuitem value="48" label="48"/>
+ <menuitem value="56" label="56"/>
+ <menuitem value="64" label="64"/>
+ <menuitem value="72" label="72"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <button id="advancedFonts"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="advanced-fonts"
+ search-l10n-ids="
+ fonts-window.title,
+ fonts-langgroup-header,
+ fonts-proportional-size,
+ fonts-proportional-header,
+ fonts-serif,
+ fonts-sans-serif,
+ fonts-monospace,
+ fonts-langgroup-arabic.label,
+ fonts-langgroup-armenian.label,
+ fonts-langgroup-bengali.label,
+ fonts-langgroup-simpl-chinese.label,
+ fonts-langgroup-trad-chinese-hk.label,
+ fonts-langgroup-trad-chinese.label,
+ fonts-langgroup-cyrillic.label,
+ fonts-langgroup-devanagari.label,
+ fonts-langgroup-ethiopic.label,
+ fonts-langgroup-georgian.label,
+ fonts-langgroup-el.label,
+ fonts-langgroup-gujarati.label,
+ fonts-langgroup-gurmukhi.label,
+ fonts-langgroup-japanese.label,
+ fonts-langgroup-hebrew.label,
+ fonts-langgroup-kannada.label,
+ fonts-langgroup-khmer.label,
+ fonts-langgroup-korean.label,
+ fonts-langgroup-latin.label,
+ fonts-langgroup-malayalam.label,
+ fonts-langgroup-math.label,
+ fonts-langgroup-odia.label,
+ fonts-langgroup-sinhala.label,
+ fonts-langgroup-tamil.label,
+ fonts-langgroup-telugu.label,
+ fonts-langgroup-thai.label,
+ fonts-langgroup-tibetan.label,
+ fonts-langgroup-canadian.label,
+ fonts-langgroup-other.label,
+ fonts-minsize,
+ fonts-minsize-none.label,
+ fonts-default-serif.label,
+ fonts-default-sans-serif.label,
+ fonts-allow-own.label,
+ " />
+ </hbox>
+</groupbox>
+
+<!-- Zoom -->
+<groupbox id="zoomGroup" data-category="paneGeneral" hidden="true">
+ <label><html:h2 data-l10n-id="preferences-zoom-header"/></label>
+
+ <hbox id="zoomBox" align="center" hidden="true">
+ <label control="defaultZoom" data-l10n-id="preferences-default-zoom"/>
+ <menulist id="defaultZoom">
+ <menupopup/>
+ </menulist>
+ </hbox>
+
+ <checkbox id="zoomText"
+ data-l10n-id="preferences-zoom-text-only"/>
+
+</groupbox>
+
+<!-- Languages -->
+<groupbox id="languagesGroup" data-category="paneGeneral" hidden="true">
+ <label><html:h2 data-l10n-id="language-header"/></label>
+
+ <vbox id="browserLanguagesBox" align="start" hidden="true">
+ <description flex="1" controls="chooseBrowserLanguage" data-l10n-id="choose-browser-language-description"/>
+ <hbox>
+ <menulist id="primaryBrowserLocale">
+ <menupopup/>
+ </menulist>
+ <button id="manageBrowserLanguagesButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="manage-browser-languages-button"/>
+ </hbox>
+ </vbox>
+ <hbox id="confirmBrowserLanguage" class="message-bar" align="center" hidden="true">
+ <html:img class="message-bar-icon"/>
+ <vbox class="message-bar-content-container" align="stretch" flex="1"/>
+ </hbox>
+
+ <hbox id="languagesBox" align="center">
+ <description flex="1" control="chooseLanguage" data-l10n-id="choose-language-description"/>
+ <button id="chooseLanguage"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="choose-button"
+ search-l10n-ids="
+ webpage-languages-window2.title,
+ languages-description,
+ languages-customize-moveup.label,
+ languages-customize-movedown.label,
+ languages-customize-remove.label,
+ languages-customize-select-language.placeholder,
+ languages-customize-add.label,
+ " />
+ </hbox>
+
+ <checkbox id="useSystemLocale" hidden="true"
+ data-l10n-id="use-system-locale"
+ data-l10n-args='{"localeName": "und"}'
+ preference="intl.regional_prefs.use_os_locales"/>
+
+ <!-- TODO (Bug 1817084) This older implementation will be removed soon -->
+ <hbox id="translationBox" hidden="true">
+ <hbox align="center" flex="1">
+ <checkbox id="translate" preference="browser.translation.detectLanguage"
+ data-l10n-id="translate-web-pages"/>
+ <hbox id="bingAttribution" hidden="true" align="center">
+ <label data-l10n-id="translate-attribution">
+ <html:img id="translationAttributionImage" aria-label="Microsoft Translator"
+ src="chrome://browser/content/microsoft-translator-attribution.png"
+ data-l10n-name="logo"/>
+ </label>
+ </hbox>
+ </hbox>
+ <button id="translateButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="translate-exceptions"/>
+ </hbox>
+
+ <hbox id="fxtranslationsBox" hidden="true" data-subcategory="fxtranslations">
+ <description flex="1" control="fxtranslateButton" data-l10n-id="fx-translate-web-pages"/>
+ <button id="fxtranslateButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="translate-exceptions"/>
+ </hbox>
+
+ <checkbox id="checkSpelling"
+ data-l10n-id="check-user-spelling"
+ preference="layout.spellcheckDefault"/>
+
+ <!-- Translations -->
+ <vbox id="translationsGroup" hidden="true" data-subcategory="translations">
+ <label><html:h2 data-l10n-id="translations-manage-header"/></label>
+ <hbox id="translations-manage-description" align="center">
+ <description flex="1" data-l10n-id="translations-manage-description"/>
+ <button id="translations-manage-settings-button"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="translations-manage-settings-button"/>
+ </hbox>
+ <vbox>
+ <html:div id="translations-manage-install-list" hidden="true">
+ <hbox class="translations-manage-language">
+ <label data-l10n-id="translations-manage-all-language"></label>
+ <button id="translations-manage-install-all"
+ data-l10n-id="translations-manage-download-button"></button>
+ <button id="translations-manage-delete-all"
+ data-l10n-id="translations-manage-delete-button"></button>
+ </hbox>
+ <!-- The downloadable languages will be listed here. -->
+ </html:div>
+ <description id="translations-manage-error" hidden="true"></description>
+ </vbox>
+ </vbox>
+</groupbox>
+
+<!-- Files and Applications -->
+<hbox id="filesAndApplicationsCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="files-and-applications-title"/>
+</hbox>
+
+<!--Downloads-->
+<groupbox id="downloadsGroup" data-category="paneGeneral" hidden="true">
+ <label><html:h2 data-l10n-id="download-header"/></label>
+
+ <hbox id="saveWhere">
+ <label id="saveTo"
+ control="downloadFolder"
+ data-l10n-id="download-save-where"/>
+ <html:input id="downloadFolder"
+ type="text"
+ readonly="readonly"
+ aria-labelledby="saveTo"/>
+ <button id="chooseFolder"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="download-choose-folder"/>
+ </hbox>
+ <checkbox id="alwaysAsk"
+ data-l10n-id="download-always-ask-where"
+ preference="browser.download.useDownloadDir"/>
+</groupbox>
+
+<groupbox id="applicationsGroup" data-category="paneGeneral" hidden="true">
+ <label><html:h2 data-l10n-id="applications-header"/></label>
+ <description data-l10n-id="applications-description"/>
+ <search-textbox id="filter" flex="1"
+ data-l10n-id="applications-filter"
+ data-l10n-attrs="placeholder"
+ aria-controls="handlersView"/>
+
+ <listheader id="handlersViewHeader">
+ <treecol id="typeColumn" data-l10n-id="applications-type-column" value="type"
+ persist="sortDirection"
+ style="flex: 1 50%" sortDirection="ascending"/>
+ <treecol id="actionColumn" data-l10n-id="applications-action-column" value="action"
+ persist="sortDirection"
+ style="flex: 1 50%"/>
+ </listheader>
+ <richlistbox id="handlersView"
+ preference="pref.downloads.disable_button.edit_actions"/>
+ <description id="handleNewFileTypesDesc"
+ data-l10n-id="applications-handle-new-file-types-description"/>
+ <radiogroup id="handleNewFileTypes"
+ preference="browser.download.always_ask_before_handling_new_types">
+ <radio id="saveForNewTypes"
+ value="false"
+ data-l10n-id="applications-save-for-new-types"/>
+ <radio id="askBeforeHandling"
+ value="true"
+ data-l10n-id="applications-ask-before-handling"/>
+ </radiogroup>
+</groupbox>
+
+
+<!-- DRM Content -->
+<groupbox id="drmGroup" data-category="paneGeneral" data-subcategory="drm" hidden="true">
+ <label><html:h2 data-l10n-id="drm-content-header"/></label>
+ <hbox align="center">
+ <checkbox id="playDRMContent" preference="media.eme.enabled"
+ class="tail-with-learn-more" data-l10n-id="play-drm-content" />
+ <html:a is="moz-support-link"
+ data-l10n-id="play-drm-content-learn-more"
+ support-page="drm-content"
+ />
+ </hbox>
+</groupbox>
+
+<hbox id="updatesCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="update-application-title"/>
+</hbox>
+
+<!-- Update -->
+<groupbox id="updateApp" data-category="paneGeneral" hidden="true">
+ <label class="search-header" hidden="true"><html:h2 data-l10n-id="update-application-title"/></label>
+
+ <label data-l10n-id="update-application-description"/>
+ <hbox align="center">
+ <vbox flex="1">
+ <description id="updateAppInfo">
+ <html:a id="releasenotes" target="_blank" data-l10n-name="learn-more" class="learnMore text-link" hidden="true"/>
+ </description>
+ <description id="distribution" class="text-blurb" hidden="true"/>
+ <description id="distributionId" class="text-blurb" hidden="true"/>
+ </vbox>
+#ifdef MOZ_UPDATER
+ <button id="showUpdateHistory"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="update-history"
+ preference="app.update.disable_button.showUpdateHistory"
+ search-l10n-ids="
+ history-title,
+ history-intro
+ "/>
+#endif
+ </hbox>
+#ifdef MOZ_UPDATER
+ <vbox id="updateBox">
+ <deck id="updateDeck" orient="vertical">
+ <hbox id="checkForUpdates" align="start">
+ <spacer flex="1"/>
+ <button id="checkForUpdatesButton"
+ is="highlightable-button"
+ data-l10n-id="update-checkForUpdatesButton"/>
+ </hbox>
+ <hbox id="downloadAndInstall" align="start">
+ <spacer flex="1"/>
+ <button id="downloadAndInstallButton"
+ is="highlightable-button"/>
+ <!-- label and accesskey will be filled by JS -->
+ </hbox>
+ <hbox id="apply" align="start">
+ <spacer flex="1"/>
+ <button id="updateButton"
+ is="highlightable-button"
+ data-l10n-id="update-updateButton"/>
+ </hbox>
+ <hbox id="checkingForUpdates" align="start">
+ <html:img class="update-throbber"/>
+ <label data-l10n-id="update-checkingForUpdates"/>
+ <button data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"
+ disabled="true"/>
+ </hbox>
+ <hbox id="downloading" align="start" data-l10n-id="settings-update-downloading" data-l10n-args='{"transfer":""}'>
+ <html:img class="update-throbber" data-l10n-name="icon"/>
+ <label data-l10n-name="download-status"/>
+ </hbox>
+ <hbox id="applying" align="start">
+ <html:img class="update-throbber"/>
+ <label data-l10n-id="update-applying"/>
+ </hbox>
+ <hbox id="downloadFailed" align="start">
+ <label data-l10n-id="update-failed-main">
+ <html:a id="failedLink" target="_blank" class="learnMore text-link" data-l10n-name="failed-link-main"></html:a>
+ </label>
+ <button id="checkForUpdatesButton2"
+ data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"/>
+ </hbox>
+ <hbox id="policyDisabled" align="start">
+ <label data-l10n-id="update-adminDisabled"/>
+ <button data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"
+ disabled="true"/>
+ </hbox>
+ <hbox id="noUpdatesFound" align="start">
+ <label class="face-smile" data-l10n-id="update-noUpdatesFound"/>
+ <button id="checkForUpdatesButton3"
+ data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"/>
+ </hbox>
+ <hbox id="checkingFailed" align="start">
+ <label class="face-sad" data-l10n-id="aboutdialog-update-checking-failed"/>
+ <button id="checkForUpdatesButton4"
+ data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"/>
+ </hbox>
+ <hbox id="otherInstanceHandlingUpdates" align="start">
+ <label data-l10n-id="update-otherInstanceHandlingUpdates"/>
+ <button data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"
+ disabled="true"/>
+ </hbox>
+ <hbox id="manualUpdate" align="start">
+ <description class="face-sad" data-l10n-id="settings-update-manual-with-link" data-l10n-args='{"displayUrl":""}'>
+ <html:a class="manualLink" data-l10n-name="manual-link" target="_blank"/>
+ </description>
+ <button data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"
+ disabled="true"/>
+ </hbox>
+ <hbox id="unsupportedSystem" align="start">
+ <description flex="1" data-l10n-id="update-unsupported">
+ <label id="unsupportedLink" class="learnMore" data-l10n-name="unsupported-link" is="text-link"></label>
+ </description>
+ <button data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"
+ disabled="true"/>
+ </hbox>
+ <hbox id="restarting" align="start">
+ <html:img class="update-throbber"/>
+ <label data-l10n-id="update-restarting"/>
+ <button data-l10n-id="update-updateButton"
+ is="highlightable-button"
+ disabled="true"/>
+ </hbox>
+ <hbox id="internalError" align="start">
+ <description class="face-sad" flex="1" data-l10n-id="update-internal-error2" data-l10n-args='{"displayUrl":""}'>
+ <label class="manualLink" data-l10n-name="manual-link" is="text-link"/>
+ </description>
+ <button data-l10n-id="update-checkForUpdatesButton"
+ is="highlightable-button"
+ disabled="true"/>
+ </hbox>
+ </deck>
+ </vbox>
+#endif
+
+#ifdef MOZ_UPDATER
+ <description id="updateAllowDescription" data-l10n-id="update-application-allow-description"></description>
+ <vbox id="updateSettingsContainer" class="info-box-container">
+ <radiogroup id="updateRadioGroup">
+ <radio id="autoDesktop"
+ value="true"
+ data-l10n-id="update-application-auto"/>
+#ifdef MOZ_UPDATE_AGENT
+ <checkbox id="backgroundUpdate"
+ class="indent"
+ hidden="true"
+ data-l10n-id="update-application-background-enabled"/>
+#endif
+ <radio id="manualDesktop"
+ value="false"
+ data-l10n-id="update-application-check-choose"/>
+ </radiogroup>
+ <hbox id="updateSettingCrossUserWarningDesc" hidden="true">
+ <hbox class="info-icon-container">
+ <html:img class="info-icon"/>
+ </hbox>
+ <description id="updateSettingCrossUserWarning"
+ flex="1"
+ data-l10n-id="update-application-warning-cross-user-setting">
+ </description>
+ </hbox>
+ </vbox>
+#ifdef MOZ_MAINTENANCE_SERVICE
+ <checkbox id="useService"
+ data-l10n-id="update-application-use-service"
+ preference="app.update.service.enabled"/>
+#endif
+#ifdef NIGHTLY_BUILD
+ <checkbox id="showUpdatePrompts"
+ data-l10n-id="update-application-suppress-prompts"
+ preference="app.update.suppressPrompts"/>
+#endif
+#endif
+</groupbox>
+
+<hbox id="performanceCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="performance-title"/>
+</hbox>
+
+<!-- Performance -->
+<groupbox id="performanceGroup" data-category="paneGeneral" hidden="true">
+ <label class="search-header" hidden="true"><html:h2 data-l10n-id="performance-title"/></label>
+
+ <hbox align="center">
+ <checkbox id="useRecommendedPerformanceSettings"
+ class="tail-with-learn-more"
+ data-l10n-id="performance-use-recommended-settings-checkbox"
+ preference="browser.preferences.defaultPerformanceSettings.enabled"/>
+ <html:a is="moz-support-link"
+ data-l10n-id="performance-settings-learn-more"
+ support-page="performance"
+ />
+ </hbox>
+ <description class="indent tip-caption" data-l10n-id="performance-use-recommended-settings-desc"/>
+
+ <vbox id="performanceSettings" class="indent" hidden="true">
+ <checkbox id="allowHWAccel"
+ data-l10n-id="performance-allow-hw-accel"
+ preference="layers.acceleration.disabled"/>
+ <hbox align="center">
+ <label id="limitContentProcess" data-l10n-id="performance-limit-content-process-option" control="contentProcessCount"/>
+ <menulist id="contentProcessCount" preference="dom.ipc.processCount">
+ <menupopup>
+ <menuitem label="1" value="1"/>
+ <menuitem label="2" value="2"/>
+ <menuitem label="3" value="3"/>
+ <menuitem label="4" value="4"/>
+ <menuitem label="5" value="5"/>
+ <menuitem label="6" value="6"/>
+ <menuitem label="7" value="7"/>
+ <menuitem label="8" value="8"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <description id="contentProcessCountEnabledDescription" class="tip-caption" data-l10n-id="performance-limit-content-process-enabled-desc"/>
+ <description id="contentProcessCountDisabledDescription" class="tip-caption" data-l10n-id="performance-limit-content-process-blocked-desc">
+ <html:a class="text-link" data-l10n-name="learn-more" href="https://wiki.mozilla.org/Electrolysis"/>
+ </description>
+ </vbox>
+</groupbox>
+
+<hbox id="browsingCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="browsing-title"/>
+</hbox>
+
+<!-- Browsing -->
+<groupbox id="browsingGroup" data-category="paneGeneral" hidden="true">
+ <label class="search-header" hidden="true"><html:h2 data-l10n-id="browsing-title"/></label>
+
+ <checkbox id="useAutoScroll"
+ data-l10n-id="browsing-use-autoscroll"
+ preference="general.autoScroll"/>
+ <checkbox id="useSmoothScrolling"
+ data-l10n-id="browsing-use-smooth-scrolling"
+ preference="general.smoothScroll"/>
+#ifdef MOZ_WIDGET_GTK
+ <checkbox id="useOverlayScrollbars"
+ data-l10n-id="browsing-gtk-use-non-overlay-scrollbars"
+ preference="widget.gtk.overlay-scrollbars.enabled"/>
+#endif
+#ifdef XP_WIN
+ <checkbox id="useOnScreenKeyboard"
+ hidden="true"
+ data-l10n-id="browsing-use-onscreen-keyboard"
+ preference="ui.osk.enabled"/>
+#endif
+ <checkbox id="useCursorNavigation"
+ data-l10n-id="browsing-use-cursor-navigation"
+ preference="accessibility.browsewithcaret"/>
+ <checkbox id="searchStartTyping"
+ data-l10n-id="browsing-search-on-start-typing"
+ preference="accessibility.typeaheadfind"/>
+ <hbox id="pictureInPictureBox" align="center" hidden="true">
+ <checkbox id="pictureInPictureToggleEnabled"
+ class="tail-with-learn-more"
+ data-l10n-id="browsing-picture-in-picture-toggle-enabled"
+ preference="media.videocontrols.picture-in-picture.video-toggle.enabled"/>
+ <html:a is="moz-support-link"
+ data-l10n-id="browsing-picture-in-picture-learn-more"
+ support-page="picture-in-picture"
+ />
+ </hbox>
+ <hbox id="mediaControlBox" align="center" hidden="true">
+ <checkbox id="mediaControlToggleEnabled"
+ class="tail-with-learn-more"
+ data-l10n-id="browsing-media-control"
+ preference="media.hardwaremediakeys.enabled"/>
+ <html:a is="moz-support-link"
+ data-l10n-id="browsing-media-control-learn-more"
+ support-page="media-keyboard-control"
+ />
+ </hbox>
+ <hbox align="center" data-subcategory="cfraddons">
+ <checkbox id="cfrRecommendations"
+ class="tail-with-learn-more"
+ data-l10n-id="browsing-cfr-recommendations"
+ preference="browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons"/>
+ <html:a is="moz-support-link"
+ data-l10n-id="browsing-cfr-recommendations-learn-more"
+ support-page="extensionrecommendations"
+ />
+ </hbox>
+ <hbox align="center" data-subcategory="cfrfeatures">
+ <checkbox id="cfrRecommendations-features"
+ class="tail-with-learn-more"
+ data-l10n-id="browsing-cfr-features"
+ preference="browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features"/>
+ <html:a is="moz-support-link"
+ data-l10n-id="browsing-cfr-recommendations-learn-more"
+ support-page="extensionrecommendations"
+ />
+ </hbox>
+</groupbox>
+
+<hbox id="networkProxyCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="network-settings-title"/>
+</hbox>
+
+<!-- Network Settings-->
+<groupbox id="connectionGroup" data-category="paneGeneral" hidden="true">
+ <label class="search-header" hidden="true"><html:h2 data-l10n-id="network-settings-title"/></label>
+
+ <hbox align="center"
+ data-subcategory="netsettings">
+ <description flex="1" control="connectionSettings">
+ <html:span id="connectionSettingsDescription"/>
+ <html:a is="moz-support-link"
+ data-l10n-id="network-proxy-connection-learn-more"
+ support-page="prefs-connection-settings"
+ />
+ </description>
+ <separator orient="vertical"/>
+ <button id="connectionSettings"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="network-proxy-connection-settings"
+ search-l10n-ids="
+ connection-window2.title,
+ connection-proxy-option-no.label,
+ connection-proxy-option-auto.label,
+ connection-proxy-option-system.label,
+ connection-proxy-option-manual.label,
+ connection-proxy-http,
+ connection-proxy-https,
+ connection-proxy-http-port,
+ connection-proxy-socks,
+ connection-proxy-socks4,
+ connection-proxy-socks5,
+ connection-proxy-noproxy,
+ connection-proxy-noproxy-desc,
+ connection-proxy-https-sharing.label,
+ connection-proxy-autotype.label,
+ connection-proxy-reload.label,
+ connection-proxy-autologin.label,
+ connection-proxy-socks-remote-dns.label,
+ " />
+ </hbox>
+</groupbox>
+</html:template>
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
new file mode 100644
index 0000000000..3f5db88709
--- /dev/null
+++ b/browser/components/preferences/main.js
@@ -0,0 +1,4258 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */
+/* import-globals-from /browser/base/content/aboutDialog-appUpdater.js */
+/* global MozXULElement */
+
+ChromeUtils.defineESModuleGetters(this, {
+ BackgroundUpdate: "resource://gre/modules/BackgroundUpdate.sys.mjs",
+ MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
+});
+
+// Constants & Enumeration Values
+const TYPE_PDF = "application/pdf";
+
+const PREF_PDFJS_DISABLED = "pdfjs.disabled";
+
+// Pref for when containers is being controlled
+const PREF_CONTAINERS_EXTENSION = "privacy.userContext.extension";
+
+// Strings to identify ExtensionSettingsStore overrides
+const CONTAINERS_KEY = "privacy.containers";
+
+const PREF_USE_SYSTEM_COLORS = "browser.display.use_system_colors";
+const PREF_CONTENT_APPEARANCE =
+ "layout.css.prefers-color-scheme.content-override";
+const FORCED_COLORS_QUERY = matchMedia("(forced-colors)");
+
+const AUTO_UPDATE_CHANGED_TOPIC =
+ UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic;
+const BACKGROUND_UPDATE_CHANGED_TOPIC =
+ UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"]
+ .observerTopic;
+
+const ICON_URL_APP =
+ AppConstants.platform == "linux"
+ ? "moz-icon://dummy.exe?size=16"
+ : "chrome://browser/skin/preferences/application.png";
+
+// For CSS. Can be one of "ask", "save" or "handleInternally". If absent, the icon URL
+// was set by us to a custom handler icon and CSS should not try to override it.
+const APP_ICON_ATTR_NAME = "appHandlerIcon";
+
+Preferences.addAll([
+ // Startup
+ { id: "browser.startup.page", type: "int" },
+ { id: "browser.privatebrowsing.autostart", type: "bool" },
+
+ // Downloads
+ { id: "browser.download.useDownloadDir", type: "bool", inverted: true },
+ { id: "browser.download.always_ask_before_handling_new_types", type: "bool" },
+ { id: "browser.download.folderList", type: "int" },
+ { id: "browser.download.dir", type: "file" },
+
+ /* Tab preferences
+ Preferences:
+
+ browser.link.open_newwindow
+ 1 opens such links in the most recent window or tab,
+ 2 opens such links in a new window,
+ 3 opens such links in a new tab
+ browser.tabs.loadInBackground
+ - true if display should switch to a new tab which has been opened from a
+ link, false if display shouldn't switch
+ browser.tabs.warnOnClose
+ - true if when closing a window with multiple tabs the user is warned and
+ allowed to cancel the action, false to just close the window
+ browser.tabs.warnOnOpen
+ - true if the user should be warned if he attempts to open a lot of tabs at
+ once (e.g. a large folder of bookmarks), false otherwise
+ browser.warnOnQuitShortcut
+ - true if the user should be warned if they quit using the keyboard shortcut
+ browser.taskbar.previews.enable
+ - true if tabs are to be shown in the Windows 7 taskbar
+ */
+
+ { id: "browser.link.open_newwindow", type: "int" },
+ { id: "browser.tabs.loadInBackground", type: "bool", inverted: true },
+ { id: "browser.tabs.warnOnClose", type: "bool" },
+ { id: "browser.warnOnQuitShortcut", type: "bool" },
+ { id: "browser.tabs.warnOnOpen", type: "bool" },
+ { id: "browser.ctrlTab.sortByRecentlyUsed", type: "bool" },
+
+ // CFR
+ {
+ id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
+ type: "bool",
+ },
+ {
+ id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ type: "bool",
+ },
+
+ // Fonts
+ { id: "font.language.group", type: "wstring" },
+
+ // Languages
+ { id: "browser.translation.detectLanguage", type: "bool" },
+ { id: "intl.regional_prefs.use_os_locales", type: "bool" },
+
+ // General tab
+
+ /* Accessibility
+ * accessibility.browsewithcaret
+ - true enables keyboard navigation and selection within web pages using a
+ visible caret, false uses normal keyboard navigation with no caret
+ * accessibility.typeaheadfind
+ - when set to true, typing outside text areas and input boxes will
+ automatically start searching for what's typed within the current
+ document; when set to false, no search action happens */
+ { id: "accessibility.browsewithcaret", type: "bool" },
+ { id: "accessibility.typeaheadfind", type: "bool" },
+ { id: "accessibility.blockautorefresh", type: "bool" },
+
+ /* Browsing
+ * general.autoScroll
+ - when set to true, clicking the scroll wheel on the mouse activates a
+ mouse mode where moving the mouse down scrolls the document downward with
+ speed correlated with the distance of the cursor from the original
+ position at which the click occurred (and likewise with movement upward);
+ if false, this behavior is disabled
+ * general.smoothScroll
+ - set to true to enable finer page scrolling than line-by-line on page-up,
+ page-down, and other such page movements */
+ { id: "general.autoScroll", type: "bool" },
+ { id: "general.smoothScroll", type: "bool" },
+ { id: "widget.gtk.overlay-scrollbars.enabled", type: "bool", inverted: true },
+ { id: "layout.spellcheckDefault", type: "int" },
+
+ {
+ id: "browser.preferences.defaultPerformanceSettings.enabled",
+ type: "bool",
+ },
+ { id: "dom.ipc.processCount", type: "int" },
+ { id: "dom.ipc.processCount.web", type: "int" },
+ { id: "layers.acceleration.disabled", type: "bool", inverted: true },
+
+ // Files and Applications
+ { id: "pref.downloads.disable_button.edit_actions", type: "bool" },
+
+ // DRM content
+ { id: "media.eme.enabled", type: "bool" },
+
+ // Update
+ { id: "browser.preferences.advanced.selectedTabIndex", type: "int" },
+ { id: "browser.search.update", type: "bool" },
+
+ { id: "privacy.userContext.enabled", type: "bool" },
+ {
+ id: "privacy.userContext.newTabContainerOnLeftClick.enabled",
+ type: "bool",
+ },
+
+ // Picture-in-Picture
+ {
+ id: "media.videocontrols.picture-in-picture.video-toggle.enabled",
+ type: "bool",
+ },
+
+ // Media
+ { id: "media.hardwaremediakeys.enabled", type: "bool" },
+]);
+
+if (AppConstants.HAVE_SHELL_SERVICE) {
+ Preferences.addAll([
+ { id: "browser.shell.checkDefaultBrowser", type: "bool" },
+ { id: "pref.general.disable_button.default_browser", type: "bool" },
+ ]);
+}
+
+if (AppConstants.platform === "win") {
+ Preferences.addAll([
+ { id: "browser.taskbar.previews.enable", type: "bool" },
+ { id: "ui.osk.enabled", type: "bool" },
+ ]);
+}
+
+if (AppConstants.MOZ_UPDATER) {
+ Preferences.addAll([
+ { id: "app.update.disable_button.showUpdateHistory", type: "bool" },
+ ]);
+
+ if (AppConstants.NIGHTLY_BUILD) {
+ Preferences.addAll([{ id: "app.update.suppressPrompts", type: "bool" }]);
+ }
+
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ Preferences.addAll([{ id: "app.update.service.enabled", type: "bool" }]);
+ }
+}
+
+XPCOMUtils.defineLazyGetter(this, "gIsPackagedApp", () => {
+ return Services.sysinfo.getProperty("isPackagedApp");
+});
+
+// A promise that resolves when the list of application handlers is loaded.
+// We store this in a global so tests can await it.
+var promiseLoadHandlersList;
+
+// Load the preferences string bundle for other locales with fallbacks.
+function getBundleForLocales(newLocales) {
+ let locales = Array.from(
+ new Set([
+ ...newLocales,
+ ...Services.locale.requestedLocales,
+ Services.locale.lastFallbackLocale,
+ ])
+ );
+ return new Localization(
+ ["browser/preferences/preferences.ftl", "branding/brand.ftl"],
+ false,
+ undefined,
+ locales
+ );
+}
+
+var gNodeToObjectMap = new WeakMap();
+
+var gMainPane = {
+ // The set of types the app knows how to handle. A hash of HandlerInfoWrapper
+ // objects, indexed by type.
+ _handledTypes: {},
+
+ // The list of types we can show, sorted by the sort column/direction.
+ // An array of HandlerInfoWrapper objects. We build this list when we first
+ // load the data and then rebuild it when users change a pref that affects
+ // what types we can show or change the sort column/direction.
+ // Note: this isn't necessarily the list of types we *will* show; if the user
+ // provides a filter string, we'll only show the subset of types in this list
+ // that match that string.
+ _visibleTypes: [],
+
+ // browser.startup.page values
+ STARTUP_PREF_BLANK: 0,
+ STARTUP_PREF_HOMEPAGE: 1,
+ STARTUP_PREF_RESTORE_SESSION: 3,
+
+ // Convenience & Performance Shortcuts
+
+ get _list() {
+ delete this._list;
+ return (this._list = document.getElementById("handlersView"));
+ },
+
+ get _filter() {
+ delete this._filter;
+ return (this._filter = document.getElementById("filter"));
+ },
+
+ _backoffIndex: 0,
+
+ /**
+ * Initialization of gMainPane.
+ */
+ init() {
+ function setEventListener(aId, aEventType, aCallback) {
+ document
+ .getElementById(aId)
+ .addEventListener(aEventType, aCallback.bind(gMainPane));
+ }
+
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ this.updateSetDefaultBrowser();
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // Exponential backoff mechanism will delay the polling times if user doesn't
+ // trigger SetDefaultBrowser for a long time.
+ let backoffTimes = [
+ 1000, 1000, 1000, 1000, 2000, 2000, 2000, 5000, 5000, 10000,
+ ];
+
+ let pollForDefaultBrowser = () => {
+ let uri = win.gBrowser.currentURI.spec;
+
+ if (
+ (uri == "about:preferences" || uri == "about:preferences#general") &&
+ document.visibilityState == "visible"
+ ) {
+ this.updateSetDefaultBrowser();
+ }
+
+ // approximately a "requestIdleInterval"
+ window.setTimeout(() => {
+ window.requestIdleCallback(pollForDefaultBrowser);
+ }, backoffTimes[this._backoffIndex + 1 < backoffTimes.length ? this._backoffIndex++ : backoffTimes.length - 1]);
+ };
+
+ window.setTimeout(() => {
+ window.requestIdleCallback(pollForDefaultBrowser);
+ }, backoffTimes[this._backoffIndex]);
+ }
+
+ this.initBrowserContainers();
+ this.buildContentProcessCountMenuList();
+
+ this.updateDefaultPerformanceSettingsPref();
+
+ let defaultPerformancePref = Preferences.get(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ );
+ defaultPerformancePref.on("change", () => {
+ this.updatePerformanceSettingsBox({ duringChangeEvent: true });
+ });
+ this.updatePerformanceSettingsBox({ duringChangeEvent: false });
+ this.displayUseSystemLocale();
+ this.updateProxySettingsUI();
+ initializeProxyUI(gMainPane);
+
+ if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
+ gMainPane.initPrimaryBrowserLanguageUI();
+ }
+
+ // We call `initDefaultZoomValues` to set and unhide the
+ // default zoom preferences menu, and to establish a
+ // listener for future menu changes.
+ gMainPane.initDefaultZoomValues();
+
+ gMainPane.initTranslations();
+
+ if (
+ Services.prefs.getBoolPref(
+ "media.videocontrols.picture-in-picture.enabled"
+ )
+ ) {
+ document.getElementById("pictureInPictureBox").hidden = false;
+ setEventListener(
+ "pictureInPictureToggleEnabled",
+ "command",
+ function (event) {
+ if (!event.target.checked) {
+ Services.telemetry.recordEvent(
+ "pictureinpicture.settings",
+ "disable",
+ "settings"
+ );
+ }
+ }
+ );
+ }
+
+ if (AppConstants.platform == "win") {
+ // Functionality for "Show tabs in taskbar" on Windows 7 and up.
+ try {
+ let ver = parseFloat(Services.sysinfo.getProperty("version"));
+ let showTabsInTaskbar = document.getElementById("showTabsInTaskbar");
+ showTabsInTaskbar.hidden = ver < 6.1;
+ } catch (ex) {}
+ }
+
+ // The "opening multiple tabs might slow down Firefox" warning provides
+ // an option for not showing this warning again. When the user disables it,
+ // we provide checkboxes to re-enable the warning.
+ if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen")) {
+ document.getElementById("warnOpenMany").hidden = true;
+ }
+
+ if (AppConstants.platform != "win") {
+ let quitKeyElement =
+ window.browsingContext.topChromeWindow.document.getElementById(
+ "key_quitApplication"
+ );
+ if (quitKeyElement) {
+ let quitKey = ShortcutUtils.prettifyShortcut(quitKeyElement);
+ document.l10n.setAttributes(
+ document.getElementById("warnOnQuitKey"),
+ "confirm-on-quit-with-key",
+ { quitKey }
+ );
+ } else {
+ // If the quit key element does not exist, then the quit key has
+ // been disabled, so just hide the checkbox.
+ document.getElementById("warnOnQuitKey").hidden = true;
+ }
+ }
+
+ setEventListener("ctrlTabRecentlyUsedOrder", "command", function () {
+ Services.prefs.clearUserPref("browser.ctrlTab.migrated");
+ });
+ setEventListener("manageBrowserLanguagesButton", "command", function () {
+ gMainPane.showBrowserLanguagesSubDialog({ search: false });
+ });
+ if (AppConstants.MOZ_UPDATER) {
+ // These elements are only compiled in when the updater is enabled
+ setEventListener("checkForUpdatesButton", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ setEventListener("downloadAndInstallButton", "command", function () {
+ gAppUpdater.startDownload();
+ });
+ setEventListener("updateButton", "command", function () {
+ gAppUpdater.buttonRestartAfterDownload();
+ });
+ setEventListener("checkForUpdatesButton2", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ setEventListener("checkForUpdatesButton3", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ setEventListener("checkForUpdatesButton4", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ }
+
+ // Startup pref
+ setEventListener(
+ "browserRestoreSession",
+ "command",
+ gMainPane.onBrowserRestoreSessionChange
+ );
+ gMainPane.updateBrowserStartupUI =
+ gMainPane.updateBrowserStartupUI.bind(gMainPane);
+ Preferences.get("browser.privatebrowsing.autostart").on(
+ "change",
+ gMainPane.updateBrowserStartupUI
+ );
+ Preferences.get("browser.startup.page").on(
+ "change",
+ gMainPane.updateBrowserStartupUI
+ );
+ Preferences.get("browser.startup.homepage").on(
+ "change",
+ gMainPane.updateBrowserStartupUI
+ );
+ gMainPane.updateBrowserStartupUI();
+
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ setEventListener(
+ "setDefaultButton",
+ "command",
+ gMainPane.setDefaultBrowser
+ );
+ }
+ setEventListener(
+ "disableContainersExtension",
+ "command",
+ makeDisableControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY)
+ );
+ setEventListener("chooseLanguage", "command", gMainPane.showLanguages);
+ setEventListener(
+ "translationAttributionImage",
+ "click",
+ gMainPane.openTranslationProviderAttribution
+ );
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ setEventListener(
+ "translateButton",
+ "command",
+ gMainPane.showTranslationExceptions
+ );
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ setEventListener(
+ "fxtranslateButton",
+ "command",
+ gMainPane.showTranslationExceptions
+ );
+ Preferences.get("font.language.group").on(
+ "change",
+ gMainPane._rebuildFonts.bind(gMainPane)
+ );
+ setEventListener("advancedFonts", "command", gMainPane.configureFonts);
+ setEventListener("colors", "command", gMainPane.configureColors);
+ Preferences.get("layers.acceleration.disabled").on(
+ "change",
+ gMainPane.updateHardwareAcceleration.bind(gMainPane)
+ );
+ setEventListener(
+ "connectionSettings",
+ "command",
+ gMainPane.showConnections
+ );
+ setEventListener(
+ "browserContainersCheckbox",
+ "command",
+ gMainPane.checkBrowserContainers
+ );
+ setEventListener(
+ "browserContainersSettings",
+ "command",
+ gMainPane.showContainerSettings
+ );
+ setEventListener(
+ "data-migration",
+ "command",
+ gMainPane.onMigrationButtonCommand
+ );
+
+ document
+ .getElementById("migrationWizardDialog")
+ .addEventListener("MigrationWizard:Close", function (e) {
+ e.currentTarget.close();
+ });
+
+ if (Services.policies && !Services.policies.isAllowed("profileImport")) {
+ document.getElementById("dataMigrationGroup").remove();
+ }
+
+ // For media control toggle button, we support it on Windows 8.1+ (NT6.3),
+ // MacOs 10.4+ (darwin8.0, but we already don't support that) and
+ // gtk-based Linux.
+ if (
+ AppConstants.isPlatformAndVersionAtLeast("win", "6.3") ||
+ AppConstants.platform == "macosx" ||
+ AppConstants.MOZ_WIDGET_GTK
+ ) {
+ document.getElementById("mediaControlBox").hidden = false;
+ }
+
+ // Initializes the fonts dropdowns displayed in this pane.
+ this._rebuildFonts();
+
+ this.updateOnScreenKeyboardVisibility();
+
+ // Show translation preferences if we may:
+ const translationsPrefName = "browser.translation.ui.show";
+ if (Services.prefs.getBoolPref(translationsPrefName)) {
+ let row = document.getElementById("translationBox");
+ row.removeAttribute("hidden");
+ // Showing attribution only for Bing Translator.
+ var { Translation } = ChromeUtils.import(
+ "resource:///modules/translation/TranslationParent.jsm"
+ );
+ if (Translation.translationEngine == "Bing") {
+ document.getElementById("bingAttribution").removeAttribute("hidden");
+ }
+ }
+
+ // Firefox Translations settings panel
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ const fxtranslationsDisabledPrefName = "extensions.translations.disabled";
+ if (!Services.prefs.getBoolPref(fxtranslationsDisabledPrefName, true)) {
+ let fxtranslationRow = document.getElementById("fxtranslationsBox");
+ fxtranslationRow.hidden = false;
+ }
+
+ let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled");
+ // Force-disable/hide on WinXP:
+ if (navigator.platform.toLowerCase().startsWith("win")) {
+ emeUIEnabled =
+ emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6;
+ }
+ if (!emeUIEnabled) {
+ // Don't want to rely on .hidden for the toplevel groupbox because
+ // of the pane hiding/showing code potentially interfering:
+ document
+ .getElementById("drmGroup")
+ .setAttribute("style", "display: none !important");
+ }
+ // Initialize the Firefox Updates section.
+ let version = AppConstants.MOZ_APP_VERSION_DISPLAY;
+
+ // Include the build ID if this is an "a#" (nightly) build
+ if (/a\d+$/.test(version)) {
+ let buildID = Services.appinfo.appBuildID;
+ let year = buildID.slice(0, 4);
+ let month = buildID.slice(4, 6);
+ let day = buildID.slice(6, 8);
+ version += ` (${year}-${month}-${day})`;
+ }
+
+ // Append "(32-bit)" or "(64-bit)" build architecture to the version number:
+ let bundle = Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ );
+ let archResource = Services.appinfo.is64Bit
+ ? "aboutDialog.architecture.sixtyFourBit"
+ : "aboutDialog.architecture.thirtyTwoBit";
+ let arch = bundle.GetStringFromName(archResource);
+ version += ` (${arch})`;
+
+ document.l10n.setAttributes(
+ document.getElementById("updateAppInfo"),
+ "update-application-version",
+ { version }
+ );
+
+ // Show a release notes link if we have a URL.
+ let relNotesLink = document.getElementById("releasenotes");
+ let relNotesPrefType = Services.prefs.getPrefType("app.releaseNotesURL");
+ if (relNotesPrefType != Services.prefs.PREF_INVALID) {
+ let relNotesURL = Services.urlFormatter.formatURLPref(
+ "app.releaseNotesURL"
+ );
+ if (relNotesURL != "about:blank") {
+ relNotesLink.href = relNotesURL;
+ relNotesLink.hidden = false;
+ }
+ }
+
+ let defaults = Services.prefs.getDefaultBranch(null);
+ let distroId = defaults.getCharPref("distribution.id", "");
+ if (distroId) {
+ let distroString = distroId;
+
+ let distroVersion = defaults.getCharPref("distribution.version", "");
+ if (distroVersion) {
+ distroString += " - " + distroVersion;
+ }
+
+ let distroIdField = document.getElementById("distributionId");
+ distroIdField.value = distroString;
+ distroIdField.hidden = false;
+
+ let distroAbout = defaults.getStringPref("distribution.about", "");
+ if (distroAbout) {
+ let distroField = document.getElementById("distribution");
+ distroField.value = distroAbout;
+ distroField.hidden = false;
+ }
+ }
+
+ if (AppConstants.MOZ_UPDATER) {
+ gAppUpdater = new appUpdater();
+ setEventListener("showUpdateHistory", "command", gMainPane.showUpdates);
+
+ let updateDisabled =
+ Services.policies && !Services.policies.isAllowed("appUpdate");
+
+ if (gIsPackagedApp) {
+ // When we're running inside an app package, there's no point in
+ // displaying any update content here, and it would get confusing if we
+ // did, because our updater is not enabled.
+ // We can't rely on the hidden attribute for the toplevel elements,
+ // because of the pane hiding/showing code interfering.
+ document
+ .getElementById("updatesCategory")
+ .setAttribute("style", "display: none !important");
+ document
+ .getElementById("updateApp")
+ .setAttribute("style", "display: none !important");
+ } else if (
+ updateDisabled ||
+ UpdateUtils.appUpdateAutoSettingIsLocked() ||
+ gApplicationUpdateService.manualUpdateOnly
+ ) {
+ document.getElementById("updateAllowDescription").hidden = true;
+ document.getElementById("updateSettingsContainer").hidden = true;
+ if (updateDisabled && AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ document.getElementById("useService").hidden = true;
+ }
+ } else {
+ // Start with no option selected since we are still reading the value
+ document.getElementById("autoDesktop").removeAttribute("selected");
+ document.getElementById("manualDesktop").removeAttribute("selected");
+ // Start reading the correct value from the disk
+ this.readUpdateAutoPref();
+ setEventListener("updateRadioGroup", "command", event => {
+ if (event.target.id == "backgroundUpdate") {
+ this.writeBackgroundUpdatePref();
+ } else {
+ this.writeUpdateAutoPref();
+ }
+ });
+ if (this.isBackgroundUpdateUIAvailable()) {
+ document.getElementById("backgroundUpdate").hidden = false;
+ // Start reading the background update pref's value from the disk.
+ this.readBackgroundUpdatePref();
+ }
+ }
+
+ if (AppConstants.platform == "win") {
+ // On Windows, the Application Update setting is an installation-
+ // specific preference, not a profile-specific one. Show a warning to
+ // inform users of this.
+ let updateContainer = document.getElementById(
+ "updateSettingsContainer"
+ );
+ updateContainer.classList.add("updateSettingCrossUserWarningContainer");
+ document.getElementById(
+ "updateSettingCrossUserWarningDesc"
+ ).hidden = false;
+ }
+
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ // Check to see if the maintenance service is installed.
+ // If it isn't installed, don't show the preference at all.
+ let installed;
+ try {
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ wrk.open(
+ wrk.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\MaintenanceService",
+ wrk.ACCESS_READ | wrk.WOW64_64
+ );
+ installed = wrk.readIntValue("Installed");
+ wrk.close();
+ } catch (e) {}
+ if (installed != 1) {
+ document.getElementById("useService").hidden = true;
+ }
+ }
+ }
+
+ // Initilize Application section.
+
+ // Observe preferences that influence what we display so we can rebuild
+ // the view when they change.
+ Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
+ Services.obs.addObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC);
+
+ setEventListener("filter", "command", gMainPane.filter);
+ setEventListener("typeColumn", "click", gMainPane.sort);
+ setEventListener("actionColumn", "click", gMainPane.sort);
+ setEventListener("chooseFolder", "command", gMainPane.chooseFolder);
+ Preferences.get("browser.download.folderList").on(
+ "change",
+ gMainPane.displayDownloadDirPref.bind(gMainPane)
+ );
+ Preferences.get("browser.download.dir").on(
+ "change",
+ gMainPane.displayDownloadDirPref.bind(gMainPane)
+ );
+ gMainPane.displayDownloadDirPref();
+
+ // Listen for window unload so we can remove our preference observers.
+ window.addEventListener("unload", this);
+
+ // Figure out how we should be sorting the list. We persist sort settings
+ // across sessions, so we can't assume the default sort column/direction.
+ // XXX should we be using the XUL sort service instead?
+ if (document.getElementById("actionColumn").hasAttribute("sortDirection")) {
+ this._sortColumn = document.getElementById("actionColumn");
+ // The typeColumn element always has a sortDirection attribute,
+ // either because it was persisted or because the default value
+ // from the xul file was used. If we are sorting on the other
+ // column, we should remove it.
+ document.getElementById("typeColumn").removeAttribute("sortDirection");
+ } else {
+ this._sortColumn = document.getElementById("typeColumn");
+ }
+
+ let browserBundle = document.getElementById("browserBundle");
+ appendSearchKeywords("browserContainersSettings", [
+ browserBundle.getString("userContextPersonal.label"),
+ browserBundle.getString("userContextWork.label"),
+ browserBundle.getString("userContextBanking.label"),
+ browserBundle.getString("userContextShopping.label"),
+ ]);
+
+ AppearanceChooser.init();
+
+ // Notify observers that the UI is now ready
+ Services.obs.notifyObservers(window, "main-pane-loaded");
+
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("defaultFont"),
+ element => FontBuilder.readFontSelection(element)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("translate"),
+ () =>
+ this.updateButtons(
+ "translateButton",
+ "browser.translation.detectLanguage"
+ )
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("checkSpelling"),
+ () => this.readCheckSpelling()
+ );
+ Preferences.addSyncToPrefListener(
+ document.getElementById("checkSpelling"),
+ () => this.writeCheckSpelling()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("alwaysAsk"),
+ () => this.readUseDownloadDir()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("linkTargeting"),
+ () => this.readLinkTarget()
+ );
+ Preferences.addSyncToPrefListener(
+ document.getElementById("linkTargeting"),
+ () => this.writeLinkTarget()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("browserContainersCheckbox"),
+ () => this.readBrowserContainersCheckbox()
+ );
+
+ this.setInitialized();
+ },
+
+ preInit() {
+ promiseLoadHandlersList = new Promise((resolve, reject) => {
+ // Load the data and build the list of handlers for applications pane.
+ // By doing this after pageshow, we ensure it doesn't delay painting
+ // of the preferences page.
+ window.addEventListener(
+ "pageshow",
+ async () => {
+ await this.initialized;
+ try {
+ this._initListEventHandlers();
+ this._loadData();
+ await this._rebuildVisibleTypes();
+ await this._rebuildView();
+ await this._sortListView();
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ },
+ { once: true }
+ );
+ });
+ },
+
+ handleSubcategory(subcategory) {
+ if (Services.policies && !Services.policies.isAllowed("profileImport")) {
+ return false;
+ }
+ if (subcategory == "migrate") {
+ this.showMigrationWizardDialog();
+ return true;
+ }
+
+ if (subcategory == "migrate-autoclose") {
+ this.showMigrationWizardDialog({ closeTabWhenDone: true });
+ }
+
+ return false;
+ },
+
+ // CONTAINERS
+
+ /*
+ * preferences:
+ *
+ * privacy.userContext.enabled
+ * - true if containers is enabled
+ */
+
+ /**
+ * Enables/disables the Settings button used to configure containers
+ */
+ readBrowserContainersCheckbox() {
+ const pref = Preferences.get("privacy.userContext.enabled");
+ const settings = document.getElementById("browserContainersSettings");
+
+ settings.disabled = !pref.value;
+ const containersEnabled = Services.prefs.getBoolPref(
+ "privacy.userContext.enabled"
+ );
+ const containersCheckbox = document.getElementById(
+ "browserContainersCheckbox"
+ );
+ containersCheckbox.checked = containersEnabled;
+ handleControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY).then(
+ isControlled => {
+ containersCheckbox.disabled = isControlled;
+ }
+ );
+ },
+
+ /**
+ * Show the Containers UI depending on the privacy.userContext.ui.enabled pref.
+ */
+ initBrowserContainers() {
+ if (!Services.prefs.getBoolPref("privacy.userContext.ui.enabled")) {
+ // The browserContainersGroup element has its own internal padding that
+ // is visible even if the browserContainersbox is visible, so hide the whole
+ // groupbox if the feature is disabled to prevent a gap in the preferences.
+ document
+ .getElementById("browserContainersbox")
+ .setAttribute("data-hidden-from-search", "true");
+ return;
+ }
+ Services.prefs.addObserver(PREF_CONTAINERS_EXTENSION, this);
+
+ document.getElementById("browserContainersbox").hidden = false;
+ this.readBrowserContainersCheckbox();
+ },
+
+ async onGetStarted(aEvent) {
+ if (!AppConstants.MOZ_DEV_EDITION) {
+ return;
+ }
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!win) {
+ return;
+ }
+ const user = await fxAccounts.getSignedInUser();
+ if (user) {
+ // We have a user, open Sync preferences in the same tab
+ win.openTrustedLinkIn("about:preferences#sync", "current");
+ return;
+ }
+ if (!(await FxAccounts.canConnectAccount())) {
+ return;
+ }
+ let url = await FxAccounts.config.promiseConnectAccountURI(
+ "dev-edition-setup"
+ );
+ let accountsTab = win.gBrowser.addWebTab(url);
+ win.gBrowser.selectedTab = accountsTab;
+ },
+
+ // HOME PAGE
+ /*
+ * Preferences:
+ *
+ * browser.startup.page
+ * - what page(s) to show when the user starts the application, as an integer:
+ *
+ * 0: a blank page (DEPRECATED - this can be set via browser.startup.homepage)
+ * 1: the home page (as set by the browser.startup.homepage pref)
+ * 2: the last page the user visited (DEPRECATED)
+ * 3: windows and tabs from the last session (a.k.a. session restore)
+ *
+ * The deprecated option is not exposed in UI; however, if the user has it
+ * selected and doesn't change the UI for this preference, the deprecated
+ * option is preserved.
+ */
+
+ /**
+ * Utility function to enable/disable the button specified by aButtonID based
+ * on the value of the Boolean preference specified by aPreferenceID.
+ */
+ updateButtons(aButtonID, aPreferenceID) {
+ var button = document.getElementById(aButtonID);
+ var preference = Preferences.get(aPreferenceID);
+ button.disabled = !preference.value;
+ return undefined;
+ },
+
+ /**
+ * Hide/show the "Show my windows and tabs from last time" option based
+ * on the value of the browser.privatebrowsing.autostart pref.
+ */
+ updateBrowserStartupUI() {
+ const pbAutoStartPref = Preferences.get(
+ "browser.privatebrowsing.autostart"
+ );
+ const startupPref = Preferences.get("browser.startup.page");
+
+ let newValue;
+ let checkbox = document.getElementById("browserRestoreSession");
+ checkbox.disabled = pbAutoStartPref.value || startupPref.locked;
+ newValue = pbAutoStartPref.value
+ ? false
+ : startupPref.value === this.STARTUP_PREF_RESTORE_SESSION;
+ if (checkbox.checked !== newValue) {
+ checkbox.checked = newValue;
+ }
+ },
+ /**
+ * Fetch the existing default zoom value, initialise and unhide
+ * the preferences menu. This method also establishes a listener
+ * to ensure handleDefaultZoomChange is called on future menu
+ * changes.
+ */
+ async initDefaultZoomValues() {
+ let win = window.browsingContext.topChromeWindow;
+ let selected = await win.ZoomUI.getGlobalValue();
+ let menulist = document.getElementById("defaultZoom");
+
+ new SelectionChangedMenulist(menulist, event => {
+ let parsedZoom = parseFloat((event.target.value / 100).toFixed(2));
+ gMainPane.handleDefaultZoomChange(parsedZoom);
+ });
+
+ setEventListener("zoomText", "command", function () {
+ win.ZoomManager.toggleZoom();
+ });
+
+ let zoomValues = win.ZoomManager.zoomValues.map(a => {
+ return Math.round(a * 100);
+ });
+
+ let fragment = document.createDocumentFragment();
+ for (let zoomLevel of zoomValues) {
+ let menuitem = document.createXULElement("menuitem");
+ document.l10n.setAttributes(menuitem, "preferences-default-zoom-value", {
+ percentage: zoomLevel,
+ });
+ menuitem.setAttribute("value", zoomLevel);
+ fragment.appendChild(menuitem);
+ }
+
+ let menupopup = menulist.querySelector("menupopup");
+ menupopup.appendChild(fragment);
+ menulist.value = Math.round(selected * 100);
+
+ let checkbox = document.getElementById("zoomText");
+ checkbox.checked = !win.ZoomManager.useFullZoom;
+
+ document.getElementById("zoomBox").hidden = false;
+ },
+
+ /**
+ * Initialize the translations view.
+ */
+ async initTranslations() {
+ if (!Services.prefs.getBoolPref("browser.translations.enable")) {
+ return;
+ }
+
+ /**
+ * Which phase a language download is in.
+ *
+ * @typedef {"downloaded" | "loading" | "uninstalled"} DownloadPhase
+ */
+
+ // Immediately show the group so that the async load of the component does
+ // not cause the layout to jump. The group will be empty initially.
+ document.getElementById("translationsGroup").hidden = false;
+
+ class TranslationsState {
+ /**
+ * The fully initialized state.
+ *
+ * @param {TranslationsActor} translationsActor
+ * @param {Object} supportedLanguages
+ * @param {Array<{ langTag: string, displayName: string}} languageList
+ * @param {Map<string, DownloadPhase>} downloadPhases
+ */
+ constructor(
+ translationsActor,
+ supportedLanguages,
+ languageList,
+ downloadPhases
+ ) {
+ this.translationsActor = translationsActor;
+ this.supportedLanguages = supportedLanguages;
+ this.languageList = languageList;
+ this.downloadPhases = downloadPhases;
+ }
+
+ /**
+ * Handles all of the async initialization logic.
+ */
+ static async create() {
+ const translationsActor =
+ window.windowGlobalChild.getActor("Translations");
+ const supportedLanguages =
+ await translationsActor.getSupportedLanguages();
+ const languageList =
+ TranslationsState.getLanguageList(supportedLanguages);
+ const downloadPhases = await TranslationsState.createDownloadPhases(
+ translationsActor,
+ languageList
+ );
+
+ if (supportedLanguages.languagePairs.length === 0) {
+ throw new Error(
+ "The supported languages list was empty. RemoteSettings may not be available at the moment."
+ );
+ }
+
+ return new TranslationsState(
+ translationsActor,
+ supportedLanguages,
+ languageList,
+ downloadPhases
+ );
+ }
+
+ /**
+ * Create a unique list of languages, sorted by the display name.
+ *
+ * @param {Object} supportedLanguages
+ * @returns {Array<{ langTag: string, displayName: string}}
+ */
+ static getLanguageList(supportedLanguages) {
+ const displayNames = new Map();
+ for (const languages of [
+ supportedLanguages.fromLanguages,
+ supportedLanguages.toLanguages,
+ ]) {
+ for (const { langTag, displayName } of languages) {
+ displayNames.set(langTag, displayName);
+ }
+ }
+
+ let appLangTag = new Intl.Locale(Services.locale.appLocaleAsBCP47)
+ .language;
+
+ // Don't offer to download the app's language.
+ displayNames.delete(appLangTag);
+
+ // Sort the list of languages by the display names.
+ return [...displayNames.entries()]
+ .map(([langTag, displayName]) => ({
+ langTag,
+ displayName,
+ }))
+ .sort((a, b) => a.displayName.localeCompare(b.displayName));
+ }
+
+ /**
+ * Determine the download phase of each language file.
+ *
+ * @param {TranslationsChild} translationsActor
+ * @param {Array<{ langTag: string, displayName: string}} languageList.
+ * @returns {Map<string, DownloadPhase>} Map the language tag to whether it is downloaded.
+ */
+ static async createDownloadPhases(translationsActor, languageList) {
+ const downloadPhases = new Map();
+ for (const { langTag } of languageList) {
+ downloadPhases.set(
+ langTag,
+ (await translationsActor.hasAllFilesForLanguage(langTag))
+ ? "downloaded"
+ : "uninstalled"
+ );
+ }
+ return downloadPhases;
+ }
+ }
+
+ class TranslationsView {
+ /** @type {Map<string, XULButton>} */
+ deleteButtons = new Map();
+ /** @type {Map<string, XULButton>} */
+ downloadButtons = new Map();
+
+ /**
+ * @param {TranslationsState} state
+ */
+ constructor(state) {
+ this.state = state;
+ this.elements = {
+ settingsButton: document.getElementById(
+ "translations-manage-settings-button"
+ ),
+ installList: document.getElementById(
+ "translations-manage-install-list"
+ ),
+ installAll: document.getElementById(
+ "translations-manage-install-all"
+ ),
+ deleteAll: document.getElementById("translations-manage-delete-all"),
+ error: document.getElementById("translations-manage-error"),
+ };
+ this.setup();
+ }
+
+ setup() {
+ this.buildLanguageList();
+
+ this.elements.settingsButton.addEventListener(
+ "command",
+ gMainPane.showTranslationsSettings
+ );
+ this.elements.installAll.addEventListener(
+ "command",
+ this.handleInstallAll
+ );
+ this.elements.deleteAll.addEventListener(
+ "command",
+ this.handleDeleteAll
+ );
+ }
+
+ handleInstallAll = async () => {
+ this.hideError();
+ this.disableButtons(true);
+ try {
+ await this.state.translationsActor.downloadAllFiles();
+ this.markAllDownloadPhases("downloaded");
+ } catch (error) {
+ TranslationsView.showError(
+ "translations-manage-error-download",
+ error
+ );
+ await this.reloadDownloadPhases();
+ this.updateAllButtons();
+ }
+ this.disableButtons(false);
+ };
+
+ handleDeleteAll = async () => {
+ this.hideError();
+ this.disableButtons(true);
+ try {
+ await this.state.translationsActor.deleteAllLanguageFiles();
+ this.markAllDownloadPhases("uninstalled");
+ } catch (error) {
+ TranslationsView.showError("translations-manage-error-delete", error);
+ // The download phases are invalidated with the error and must be reloaded.
+ await this.reloadDownloadPhases();
+ console.error(error);
+ }
+ this.disableButtons(false);
+ };
+
+ /**
+ * @param {string} langTag
+ * @returns {Function}
+ */
+ getDownloadButtonHandler(langTag) {
+ return async () => {
+ this.hideError();
+ this.updateDownloadPhase(langTag, "loading");
+ try {
+ await this.state.translationsActor.downloadLanguageFiles(langTag);
+ this.updateDownloadPhase(langTag, "downloaded");
+ } catch (error) {
+ TranslationsView.showError(
+ "translations-manage-error-download",
+ error
+ );
+ this.updateDownloadPhase(langTag, "uninstalled");
+ }
+ };
+ }
+
+ /**
+ * @param {string} langTag
+ * @returns {Function}
+ */
+ getDeleteButtonHandler(langTag) {
+ return async () => {
+ this.hideError();
+ this.updateDownloadPhase(langTag, "loading");
+ try {
+ await this.state.translationsActor.deleteLanguageFiles(langTag);
+ this.updateDownloadPhase(langTag, "uninstalled");
+ } catch (error) {
+ TranslationsView.showError(
+ "translations-manage-error-delete",
+ error
+ );
+ // The download phases are invalidated with the error and must be reloaded.
+ await this.reloadDownloadPhases();
+ }
+ };
+ }
+
+ buildLanguageList() {
+ const listFragment = document.createDocumentFragment();
+
+ for (const { langTag, displayName } of this.state.languageList) {
+ const hboxRow = document.createXULElement("hbox");
+ hboxRow.classList.add("translations-manage-language");
+
+ const languageLabel = document.createXULElement("label");
+ languageLabel.textContent = displayName; // The display name is already localized.
+
+ const downloadButton = document.createXULElement("button");
+ const deleteButton = document.createXULElement("button");
+
+ downloadButton.addEventListener(
+ "command",
+ this.getDownloadButtonHandler(langTag)
+ );
+ deleteButton.addEventListener(
+ "command",
+ this.getDeleteButtonHandler(langTag)
+ );
+
+ document.l10n.setAttributes(
+ downloadButton,
+ "translations-manage-download-button"
+ );
+ document.l10n.setAttributes(
+ deleteButton,
+ "translations-manage-delete-button"
+ );
+
+ downloadButton.hidden = true;
+ deleteButton.hidden = true;
+
+ this.deleteButtons.set(langTag, deleteButton);
+ this.downloadButtons.set(langTag, downloadButton);
+
+ hboxRow.appendChild(languageLabel);
+ hboxRow.appendChild(downloadButton);
+ hboxRow.appendChild(deleteButton);
+ listFragment.appendChild(hboxRow);
+ }
+ this.updateAllButtons();
+ this.elements.installList.appendChild(listFragment);
+ this.elements.installList.hidden = false;
+ }
+
+ /**
+ * Update the DownloadPhase for a single langTag.
+ * @param {string} langTag
+ * @param {DownloadPhase} downloadPhase
+ */
+ updateDownloadPhase(langTag, downloadPhase) {
+ this.state.downloadPhases.set(langTag, downloadPhase);
+ this.updateButton(langTag, downloadPhase);
+ this.updateHeaderButtons();
+ }
+
+ /**
+ * Recreates the download map when the state is invalidated.
+ */
+ async reloadDownloadPhases() {
+ this.state.downloadPhases =
+ await TranslationsState.createDownloadPhases(
+ this.state.translationsActor,
+ this.state.languageList
+ );
+ this.updateAllButtons();
+ }
+
+ /**
+ * Set all the downloads.
+ * @param {DownloadPhase} downloadPhase
+ */
+ markAllDownloadPhases(downloadPhase) {
+ const { downloadPhases } = this.state;
+ for (const key of downloadPhases.keys()) {
+ downloadPhases.set(key, downloadPhase);
+ }
+ this.updateAllButtons();
+ }
+
+ /**
+ * If all languages are downloaded, or no languages are downloaded then
+ * the visibility of the buttons need to change.
+ */
+ updateHeaderButtons() {
+ let allDownloaded = true;
+ let allUninstalled = true;
+ for (const downloadPhase of this.state.downloadPhases.values()) {
+ if (downloadPhase === "loading") {
+ // Don't count loading towards this calculation.
+ continue;
+ }
+ allDownloaded &&= downloadPhase === "downloaded";
+ allUninstalled &&= downloadPhase === "uninstalled";
+ }
+
+ this.elements.installAll.hidden = allDownloaded;
+ this.elements.deleteAll.hidden = allUninstalled;
+ }
+
+ /**
+ * Update the buttons according to their download state.
+ */
+ updateAllButtons() {
+ this.updateHeaderButtons();
+ for (const [langTag, downloadPhase] of this.state.downloadPhases) {
+ this.updateButton(langTag, downloadPhase);
+ }
+ }
+
+ /**
+ * @param {string} langTag
+ * @param {DownloadPhase} downloadPhase
+ */
+ updateButton(langTag, downloadPhase) {
+ const downloadButton = this.downloadButtons.get(langTag);
+ const deleteButton = this.deleteButtons.get(langTag);
+ switch (downloadPhase) {
+ case "downloaded":
+ downloadButton.hidden = true;
+ deleteButton.hidden = false;
+ downloadButton.removeAttribute("disabled");
+ break;
+ case "uninstalled":
+ downloadButton.hidden = false;
+ deleteButton.hidden = true;
+ downloadButton.removeAttribute("disabled");
+ break;
+ case "loading":
+ downloadButton.hidden = false;
+ deleteButton.hidden = true;
+ downloadButton.setAttribute("disabled", true);
+ break;
+ }
+ }
+
+ /**
+ * @param {boolean} isDisabled
+ */
+ disableButtons(isDisabled) {
+ this.elements.installAll.disabled = isDisabled;
+ this.elements.deleteAll.disabled = isDisabled;
+ for (const button of this.downloadButtons.values()) {
+ button.disabled = isDisabled;
+ }
+ for (const button of this.deleteButtons.values()) {
+ button.disabled = isDisabled;
+ }
+ }
+
+ /**
+ * This method is static in case an error happens during the creation of the
+ * TranslationsState.
+ *
+ * @param {string} l10nId
+ * @param {Error} error
+ */
+ static showError(l10nId, error) {
+ console.error(error);
+ const errorMessage = document.getElementById(
+ "translations-manage-error"
+ );
+ errorMessage.hidden = false;
+ document.l10n.setAttributes(errorMessage, l10nId);
+ }
+
+ hideError() {
+ this.elements.error.hidden = true;
+ }
+ }
+
+ TranslationsState.create().then(
+ state => {
+ new TranslationsView(state);
+ },
+ error => {
+ // This error can happen when a user is not connected to the internet, or
+ // RemoteSettings is down for some reason.
+ TranslationsView.showError("translations-manage-error-list", error);
+ }
+ );
+ },
+
+ initPrimaryBrowserLanguageUI() {
+ // Enable telemetry.
+ Services.telemetry.setEventRecordingEnabled(
+ "intl.ui.browserLanguage",
+ true
+ );
+
+ // This will register the "command" listener.
+ let menulist = document.getElementById("primaryBrowserLocale");
+ new SelectionChangedMenulist(menulist, event => {
+ gMainPane.onPrimaryBrowserLanguageMenuChange(event);
+ });
+
+ gMainPane.updatePrimaryBrowserLanguageUI(Services.locale.appLocaleAsBCP47);
+ },
+
+ /**
+ * Update the available list of locales and select the locale that the user
+ * is "selecting". This could be the currently requested locale or a locale
+ * that the user would like to switch to after confirmation.
+ *
+ * @param {string} selected - The selected BCP 47 locale.
+ */
+ async updatePrimaryBrowserLanguageUI(selected) {
+ let available = await LangPackMatcher.getAvailableLocales();
+ let localeNames = Services.intl.getLocaleDisplayNames(
+ undefined,
+ available,
+ { preferNative: true }
+ );
+ let locales = available.map((code, i) => ({ code, name: localeNames[i] }));
+ locales.sort((a, b) => a.name > b.name);
+
+ let fragment = document.createDocumentFragment();
+ for (let { code, name } of locales) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("value", code);
+ menuitem.setAttribute("label", name);
+ fragment.appendChild(menuitem);
+ }
+
+ // Add an option to search for more languages if downloading is supported.
+ if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.id = "primaryBrowserLocaleSearch";
+ menuitem.setAttribute(
+ "label",
+ await document.l10n.formatValue("browser-languages-search")
+ );
+ menuitem.setAttribute("value", "search");
+ fragment.appendChild(menuitem);
+ }
+
+ let menulist = document.getElementById("primaryBrowserLocale");
+ let menupopup = menulist.querySelector("menupopup");
+ menupopup.textContent = "";
+ menupopup.appendChild(fragment);
+ menulist.value = selected;
+
+ document.getElementById("browserLanguagesBox").hidden = false;
+ },
+
+ /* Show the confirmation message bar to allow a restart into the new locales. */
+ async showConfirmLanguageChangeMessageBar(locales) {
+ let messageBar = document.getElementById("confirmBrowserLanguage");
+
+ // Get the bundle for the new locale.
+ let newBundle = getBundleForLocales(locales);
+
+ // Find the messages and labels.
+ let messages = await Promise.all(
+ [newBundle, document.l10n].map(async bundle =>
+ bundle.formatValue("confirm-browser-language-change-description")
+ )
+ );
+ let buttonLabels = await Promise.all(
+ [newBundle, document.l10n].map(async bundle =>
+ bundle.formatValue("confirm-browser-language-change-button")
+ )
+ );
+
+ // If both the message and label are the same, just include one row.
+ if (messages[0] == messages[1] && buttonLabels[0] == buttonLabels[1]) {
+ messages.pop();
+ buttonLabels.pop();
+ }
+
+ let contentContainer = messageBar.querySelector(
+ ".message-bar-content-container"
+ );
+ contentContainer.textContent = "";
+
+ for (let i = 0; i < messages.length; i++) {
+ let messageContainer = document.createXULElement("hbox");
+ messageContainer.classList.add("message-bar-content");
+ messageContainer.style.flex = "1 50%";
+ messageContainer.setAttribute("align", "center");
+
+ let description = document.createXULElement("description");
+ description.classList.add("message-bar-description");
+
+ if (i == 0 && Services.intl.getScriptDirection(locales[0]) === "rtl") {
+ description.classList.add("rtl-locale");
+ }
+ description.setAttribute("flex", "1");
+ description.textContent = messages[i];
+ messageContainer.appendChild(description);
+
+ let button = document.createXULElement("button");
+ button.addEventListener(
+ "command",
+ gMainPane.confirmBrowserLanguageChange
+ );
+ button.classList.add("message-bar-button");
+ button.setAttribute("locales", locales.join(","));
+ button.setAttribute("label", buttonLabels[i]);
+ messageContainer.appendChild(button);
+
+ contentContainer.appendChild(messageContainer);
+ }
+
+ messageBar.hidden = false;
+ gMainPane.selectedLocalesForRestart = locales;
+ },
+
+ hideConfirmLanguageChangeMessageBar() {
+ let messageBar = document.getElementById("confirmBrowserLanguage");
+ messageBar.hidden = true;
+ let contentContainer = messageBar.querySelector(
+ ".message-bar-content-container"
+ );
+ contentContainer.textContent = "";
+ gMainPane.requestingLocales = null;
+ },
+
+ /* Confirm the locale change and restart the browser in the new locale. */
+ confirmBrowserLanguageChange(event) {
+ let localesString = (event.target.getAttribute("locales") || "").trim();
+ if (!localesString || !localesString.length) {
+ return;
+ }
+ let locales = localesString.split(",");
+ Services.locale.requestedLocales = locales;
+
+ // Record the change in telemetry before we restart.
+ gMainPane.recordBrowserLanguagesTelemetry("apply");
+
+ // Restart with the new locale.
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ if (!cancelQuit.data) {
+ Services.startup.quit(
+ Services.startup.eAttemptQuit | Services.startup.eRestart
+ );
+ }
+ },
+
+ /* Show or hide the confirm change message bar based on the new locale. */
+ onPrimaryBrowserLanguageMenuChange(event) {
+ let locale = event.target.value;
+
+ if (locale == "search") {
+ gMainPane.showBrowserLanguagesSubDialog({ search: true });
+ return;
+ } else if (locale == Services.locale.appLocaleAsBCP47) {
+ this.hideConfirmLanguageChangeMessageBar();
+ return;
+ }
+
+ let newLocales = Array.from(
+ new Set([locale, ...Services.locale.requestedLocales]).values()
+ );
+
+ gMainPane.recordBrowserLanguagesTelemetry("reorder");
+
+ switch (gMainPane.getLanguageSwitchTransitionType(newLocales)) {
+ case "requires-restart":
+ // Prepare to change the locales, as they were different.
+ gMainPane.showConfirmLanguageChangeMessageBar(newLocales);
+ gMainPane.updatePrimaryBrowserLanguageUI(newLocales[0]);
+ break;
+ case "live-reload":
+ Services.locale.requestedLocales = newLocales;
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ case "locales-match":
+ // They matched, so we can reset the UI.
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ default:
+ throw new Error("Unhandled transition type.");
+ }
+ },
+
+ /**
+ * Takes as newZoom a floating point value representing the
+ * new default zoom. This value should not be a string, and
+ * should not carry a percentage sign/other localisation
+ * characteristics.
+ */
+ handleDefaultZoomChange(newZoom) {
+ let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+ let nonPrivateLoadContext = Cu.createLoadContext();
+ /* Because our setGlobal function takes in a browsing context, and
+ * because we want to keep this property consistent across both private
+ * and non-private contexts, we crate a non-private context and use that
+ * to set the property, regardless of our actual context.
+ */
+
+ let win = window.browsingContext.topChromeWindow;
+ cps2.setGlobal(win.FullZoom.name, newZoom, nonPrivateLoadContext);
+ },
+
+ onBrowserRestoreSessionChange(event) {
+ const value = event.target.checked;
+ const startupPref = Preferences.get("browser.startup.page");
+ let newValue;
+
+ if (value) {
+ // We need to restore the blank homepage setting in our other pref
+ if (startupPref.value === this.STARTUP_PREF_BLANK) {
+ HomePage.safeSet("about:blank");
+ }
+ newValue = this.STARTUP_PREF_RESTORE_SESSION;
+ } else {
+ newValue = this.STARTUP_PREF_HOMEPAGE;
+ }
+ startupPref.value = newValue;
+ },
+
+ // TABS
+
+ /*
+ * Preferences:
+ *
+ * browser.link.open_newwindow - int
+ * Determines where links targeting new windows should open.
+ * Values:
+ * 1 - Open in the current window or tab.
+ * 2 - Open in a new window.
+ * 3 - Open in a new tab in the most recent window.
+ * browser.tabs.loadInBackground - bool
+ * True - Whether browser should switch to a new tab opened from a link.
+ * browser.tabs.warnOnClose - bool
+ * True - If when closing a window with multiple tabs the user is warned and
+ * allowed to cancel the action, false to just close the window.
+ * browser.warnOnQuitShortcut - bool
+ * True - If the keyboard shortcut (Ctrl/Cmd+Q) is pressed, the user should
+ * be warned, false to just quit without prompting.
+ * browser.tabs.warnOnOpen - bool
+ * True - Whether the user should be warned when trying to open a lot of
+ * tabs at once (e.g. a large folder of bookmarks), allowing to
+ * cancel the action.
+ * browser.taskbar.previews.enable - bool
+ * True - Tabs are to be shown in Windows 7 taskbar.
+ * False - Only the window is to be shown in Windows 7 taskbar.
+ */
+
+ /**
+ * Determines where a link which opens a new window will open.
+ *
+ * @returns |true| if such links should be opened in new tabs
+ */
+ readLinkTarget() {
+ var openNewWindow = Preferences.get("browser.link.open_newwindow");
+ return openNewWindow.value != 2;
+ },
+
+ /**
+ * Determines where a link which opens a new window will open.
+ *
+ * @returns 2 if such links should be opened in new windows,
+ * 3 if such links should be opened in new tabs
+ */
+ writeLinkTarget() {
+ var linkTargeting = document.getElementById("linkTargeting");
+ return linkTargeting.checked ? 3 : 2;
+ },
+ /*
+ * Preferences:
+ *
+ * browser.shell.checkDefault
+ * - true if a default-browser check (and prompt to make it so if necessary)
+ * occurs at startup, false otherwise
+ */
+
+ /**
+ * Show button for setting browser as default browser or information that
+ * browser is already the default browser.
+ */
+ updateSetDefaultBrowser() {
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ let shellSvc = getShellService();
+ let defaultBrowserBox = document.getElementById("defaultBrowserBox");
+ let isInFlatpak = gGIOService?.isRunningUnderFlatpak;
+ // Flatpak does not support setting nor detection of default browser
+ if (!shellSvc || isInFlatpak) {
+ defaultBrowserBox.hidden = true;
+ return;
+ }
+ let isDefault = shellSvc.isDefaultBrowser(false, true);
+ let setDefaultPane = document.getElementById("setDefaultPane");
+ setDefaultPane.classList.toggle("is-default", isDefault);
+ let alwaysCheck = document.getElementById("alwaysCheckDefault");
+ let alwaysCheckPref = Preferences.get(
+ "browser.shell.checkDefaultBrowser"
+ );
+ alwaysCheck.disabled = alwaysCheckPref.locked || isDefault;
+ }
+ },
+
+ /**
+ * Set browser as the operating system default browser.
+ */
+ setDefaultBrowser() {
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ let alwaysCheckPref = Preferences.get(
+ "browser.shell.checkDefaultBrowser"
+ );
+ alwaysCheckPref.value = true;
+
+ // Reset exponential backoff delay time in order to do visual update in pollForDefaultBrowser.
+ this._backoffIndex = 0;
+
+ let shellSvc = getShellService();
+ if (!shellSvc) {
+ return;
+ }
+ try {
+ shellSvc.setDefaultBrowser(true, false);
+ } catch (ex) {
+ console.error(ex);
+ return;
+ }
+
+ let isDefault = shellSvc.isDefaultBrowser(false, true);
+ let setDefaultPane = document.getElementById("setDefaultPane");
+ setDefaultPane.classList.toggle("is-default", isDefault);
+ }
+ },
+
+ /**
+ * Shows a dialog in which the preferred language for web content may be set.
+ */
+ showLanguages() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/languages.xhtml"
+ );
+ },
+
+ recordBrowserLanguagesTelemetry(method, value = null) {
+ Services.telemetry.recordEvent(
+ "intl.ui.browserLanguage",
+ method,
+ "main",
+ value
+ );
+ },
+
+ /**
+ * Open the browser languages sub dialog in either the normal mode, or search mode.
+ * The search mode is only available from the menu to change the primary browser
+ * language.
+ *
+ * @param {{ search: boolean }}
+ */
+ showBrowserLanguagesSubDialog({ search }) {
+ // Record the telemetry event with an id to associate related actions.
+ let telemetryId = parseInt(
+ Services.telemetry.msSinceProcessStart(),
+ 10
+ ).toString();
+ let method = search ? "search" : "manage";
+ gMainPane.recordBrowserLanguagesTelemetry(method, telemetryId);
+
+ let opts = {
+ selectedLocalesForRestart: gMainPane.selectedLocalesForRestart,
+ search,
+ telemetryId,
+ };
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml",
+ { closingCallback: this.browserLanguagesClosed },
+ opts
+ );
+ },
+
+ /**
+ * Determine the transition strategy for switching the locale based on prefs
+ * and the switched locales.
+ *
+ * @param {Array<string>} newLocales - List of BCP 47 locale identifiers.
+ * @returns {"locales-match" | "requires-restart" | "live-reload"}
+ */
+ getLanguageSwitchTransitionType(newLocales) {
+ const { appLocalesAsBCP47 } = Services.locale;
+ if (appLocalesAsBCP47.join(",") === newLocales.join(",")) {
+ // The selected locales match, the order matters.
+ return "locales-match";
+ }
+
+ if (Services.prefs.getBoolPref("intl.multilingual.liveReload")) {
+ if (
+ Services.intl.getScriptDirection(newLocales[0]) !==
+ Services.intl.getScriptDirection(appLocalesAsBCP47[0]) &&
+ !Services.prefs.getBoolPref("intl.multilingual.liveReloadBidirectional")
+ ) {
+ // Bug 1750852: The directionality of the text changed, which requires a restart
+ // until the quality of the switch can be improved.
+ return "requires-restart";
+ }
+
+ return "live-reload";
+ }
+
+ return "requires-restart";
+ },
+
+ /* Show or hide the confirm change message bar based on the updated ordering. */
+ browserLanguagesClosed() {
+ // When the subdialog is closed, settings are stored on gBrowserLanguagesDialog.
+ // The next time the dialog is opened, a new gBrowserLanguagesDialog is created.
+ let { selected } = this.gBrowserLanguagesDialog;
+
+ this.gBrowserLanguagesDialog.recordTelemetry(
+ selected ? "accept" : "cancel"
+ );
+
+ if (!selected) {
+ // No locales were selected. Cancel the operation.
+ return;
+ }
+
+ switch (gMainPane.getLanguageSwitchTransitionType(selected)) {
+ case "requires-restart":
+ gMainPane.showConfirmLanguageChangeMessageBar(selected);
+ gMainPane.updatePrimaryBrowserLanguageUI(selected[0]);
+ break;
+ case "live-reload":
+ Services.locale.requestedLocales = selected;
+
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ case "locales-match":
+ // They matched, so we can reset the UI.
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ default:
+ throw new Error("Unhandled transition type.");
+ }
+ },
+
+ displayUseSystemLocale() {
+ let appLocale = Services.locale.appLocaleAsBCP47;
+ let regionalPrefsLocales = Services.locale.regionalPrefsLocales;
+ if (!regionalPrefsLocales.length) {
+ return;
+ }
+ let systemLocale = regionalPrefsLocales[0];
+ let localeDisplayname = Services.intl.getLocaleDisplayNames(
+ undefined,
+ [systemLocale],
+ { preferNative: true }
+ );
+ if (!localeDisplayname.length) {
+ return;
+ }
+ let localeName = localeDisplayname[0];
+ if (appLocale.split("-u-")[0] != systemLocale.split("-u-")[0]) {
+ let checkbox = document.getElementById("useSystemLocale");
+ document.l10n.setAttributes(checkbox, "use-system-locale", {
+ localeName,
+ });
+ checkbox.hidden = false;
+ }
+ },
+
+ /**
+ * Displays the translation exceptions dialog where specific site and language
+ * translation preferences can be set.
+ */
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ showTranslationExceptions() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/translationExceptions.xhtml"
+ );
+ },
+
+ showTranslationsSettings() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/translations.xhtml"
+ );
+ },
+
+ openTranslationProviderAttribution() {
+ var { Translation } = ChromeUtils.import(
+ "resource:///modules/translation/TranslationParent.jsm"
+ );
+ Translation.openProviderAttribution();
+ },
+
+ /**
+ * Displays the fonts dialog, where web page font names and sizes can be
+ * configured.
+ */
+ configureFonts() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/fonts.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ /**
+ * Displays the colors dialog, where default web page/link/etc. colors can be
+ * configured.
+ */
+ configureColors() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/colors.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ // NETWORK
+ /**
+ * Displays a dialog in which proxy settings may be changed.
+ */
+ showConnections() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/connection.xhtml",
+ { closingCallback: this.updateProxySettingsUI.bind(this) }
+ );
+ },
+
+ // Update the UI to show the proper description depending on whether an
+ // extension is in control or not.
+ async updateProxySettingsUI() {
+ let controllingExtension = await getControllingExtension(
+ PREF_SETTING_TYPE,
+ PROXY_KEY
+ );
+ let description = document.getElementById("connectionSettingsDescription");
+
+ if (controllingExtension) {
+ setControllingExtensionDescription(
+ description,
+ controllingExtension,
+ "proxy.settings"
+ );
+ } else {
+ setControllingExtensionDescription(
+ description,
+ null,
+ "network-proxy-connection-description"
+ );
+ }
+ },
+
+ async checkBrowserContainers(event) {
+ let checkbox = document.getElementById("browserContainersCheckbox");
+ if (checkbox.checked) {
+ Services.prefs.setBoolPref("privacy.userContext.enabled", true);
+ return;
+ }
+
+ let count = ContextualIdentityService.countContainerTabs();
+ if (count == 0) {
+ Services.prefs.setBoolPref("privacy.userContext.enabled", false);
+ return;
+ }
+
+ let [title, message, okButton, cancelButton] =
+ await document.l10n.formatValues([
+ { id: "containers-disable-alert-title" },
+ { id: "containers-disable-alert-desc", args: { tabCount: count } },
+ { id: "containers-disable-alert-ok-button", args: { tabCount: count } },
+ { id: "containers-disable-alert-cancel-button" },
+ ]);
+
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1;
+
+ let rv = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ okButton,
+ cancelButton,
+ null,
+ null,
+ {}
+ );
+ if (rv == 0) {
+ Services.prefs.setBoolPref("privacy.userContext.enabled", false);
+ return;
+ }
+
+ checkbox.checked = true;
+ },
+
+ /**
+ * Displays container panel for customising and adding containers.
+ */
+ showContainerSettings() {
+ gotoPref("containers");
+ },
+
+ /**
+ * ui.osk.enabled
+ * - when set to true, subject to other conditions, we may sometimes invoke
+ * an on-screen keyboard when a text input is focused.
+ * (Currently Windows-only, and depending on prefs, may be Windows-8-only)
+ */
+ updateOnScreenKeyboardVisibility() {
+ if (AppConstants.platform == "win") {
+ let minVersion = Services.prefs.getBoolPref("ui.osk.require_win10")
+ ? 10
+ : 6.2;
+ if (
+ Services.vc.compare(
+ Services.sysinfo.getProperty("version"),
+ minVersion
+ ) >= 0
+ ) {
+ document.getElementById("useOnScreenKeyboard").hidden = false;
+ }
+ }
+ },
+
+ updateHardwareAcceleration() {
+ // Placeholder for restart on change
+ },
+
+ // FONTS
+
+ /**
+ * Populates the default font list in UI.
+ */
+ _rebuildFonts() {
+ var langGroupPref = Preferences.get("font.language.group");
+ var isSerif =
+ this._readDefaultFontTypeForLanguage(langGroupPref.value) == "serif";
+ this._selectDefaultLanguageGroup(langGroupPref.value, isSerif);
+ },
+
+ /**
+ * Returns the type of the current default font for the language denoted by
+ * aLanguageGroup.
+ */
+ _readDefaultFontTypeForLanguage(aLanguageGroup) {
+ const kDefaultFontType = "font.default.%LANG%";
+ var defaultFontTypePref = kDefaultFontType.replace(
+ /%LANG%/,
+ aLanguageGroup
+ );
+ var preference = Preferences.get(defaultFontTypePref);
+ if (!preference) {
+ preference = Preferences.add({ id: defaultFontTypePref, type: "string" });
+ preference.on("change", gMainPane._rebuildFonts.bind(gMainPane));
+ }
+ return preference.value;
+ },
+
+ _selectDefaultLanguageGroupPromise: Promise.resolve(),
+
+ _selectDefaultLanguageGroup(aLanguageGroup, aIsSerif) {
+ this._selectDefaultLanguageGroupPromise = (async () => {
+ // Avoid overlapping language group selections by awaiting the resolution
+ // of the previous one. We do this because this function is re-entrant,
+ // as inserting <preference> elements into the DOM sometimes triggers a call
+ // back into this function. And since this function is also asynchronous,
+ // that call can enter this function before the previous run has completed,
+ // which would corrupt the font menulists. Awaiting the previous call's
+ // resolution avoids that fate.
+ await this._selectDefaultLanguageGroupPromise;
+
+ const kFontNameFmtSerif = "font.name.serif.%LANG%";
+ const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%";
+ const kFontNameListFmtSerif = "font.name-list.serif.%LANG%";
+ const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
+ const kFontSizeFmtVariable = "font.size.variable.%LANG%";
+
+ var prefs = [
+ {
+ format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif,
+ type: "fontname",
+ element: "defaultFont",
+ fonttype: aIsSerif ? "serif" : "sans-serif",
+ },
+ {
+ format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif,
+ type: "unichar",
+ element: null,
+ fonttype: aIsSerif ? "serif" : "sans-serif",
+ },
+ {
+ format: kFontSizeFmtVariable,
+ type: "int",
+ element: "defaultFontSize",
+ fonttype: null,
+ },
+ ];
+ for (var i = 0; i < prefs.length; ++i) {
+ var preference = Preferences.get(
+ prefs[i].format.replace(/%LANG%/, aLanguageGroup)
+ );
+ if (!preference) {
+ var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup);
+ preference = Preferences.add({ id: name, type: prefs[i].type });
+ }
+
+ if (!prefs[i].element) {
+ continue;
+ }
+
+ var element = document.getElementById(prefs[i].element);
+ if (element) {
+ element.setAttribute("preference", preference.id);
+
+ if (prefs[i].fonttype) {
+ await FontBuilder.buildFontList(
+ aLanguageGroup,
+ prefs[i].fonttype,
+ element
+ );
+ }
+
+ preference.setElementValue(element);
+ }
+ }
+ })().catch(console.error);
+ },
+
+ onMigrationButtonCommand(command) {
+ // When browser.migrate.content-modal.enabled is enabled by default,
+ // the event handler can just call showMigrationWizardDialog directly,
+ // but for now, we delegate to MigrationUtils to open the native modal
+ // in case that's the dialog we're still using.
+ //
+ // Enabling the pref by default will be part of bug 1822156.
+ const browser = window.docShell.chromeEventHandler;
+ const browserWindow = browser.ownerGlobal;
+
+ // showMigrationWizard blocks on some platforms. We'll dispatch the request
+ // to open to a runnable on the main thread so that we don't have to block
+ // this function call.
+ Services.tm.dispatchToMainThread(() => {
+ MigrationUtils.showMigrationWizard(browserWindow, {
+ entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES,
+ });
+ });
+ },
+
+ /**
+ * Displays the migration wizard dialog in an HTML dialog.
+ */
+ async showMigrationWizardDialog({ closeTabWhenDone = false } = {}) {
+ let migrationWizardDialog = document.getElementById(
+ "migrationWizardDialog"
+ );
+
+ if (migrationWizardDialog.open) {
+ return;
+ }
+
+ await customElements.whenDefined("migration-wizard");
+
+ // If we've been opened before, remove the old wizard and insert a
+ // new one to put it back into its starting state.
+ if (!migrationWizardDialog.firstElementChild) {
+ let wizard = document.createElement("migration-wizard");
+ wizard.toggleAttribute("dialog-mode", true);
+
+ let panelList = document.createElement("panel-list");
+ let panel = document.createXULElement("panel");
+ panel.appendChild(panelList);
+ wizard.appendChild(panel);
+
+ migrationWizardDialog.appendChild(wizard);
+ }
+ migrationWizardDialog.firstElementChild.requestState();
+
+ migrationWizardDialog.addEventListener(
+ "close",
+ () => {
+ // Let others know that the wizard is closed -- potentially because of a
+ // user action within the dialog that dispatches "MigrationWizard:Close"
+ // but this also covers cases like hitting Escape.
+ Services.obs.notifyObservers(
+ migrationWizardDialog,
+ "MigrationWizard:Closed"
+ );
+ if (closeTabWhenDone) {
+ window.close();
+ }
+ },
+ { once: true }
+ );
+
+ migrationWizardDialog.showModal();
+ },
+
+ /**
+ * Stores the original value of the spellchecking preference to enable proper
+ * restoration if unchanged (since we're mapping a tristate onto a checkbox).
+ */
+ _storedSpellCheck: 0,
+
+ /**
+ * Returns true if any spellchecking is enabled and false otherwise, caching
+ * the current value to enable proper pref restoration if the checkbox is
+ * never changed.
+ *
+ * layout.spellcheckDefault
+ * - an integer:
+ * 0 disables spellchecking
+ * 1 enables spellchecking, but only for multiline text fields
+ * 2 enables spellchecking for all text fields
+ */
+ readCheckSpelling() {
+ var pref = Preferences.get("layout.spellcheckDefault");
+ this._storedSpellCheck = pref.value;
+
+ return pref.value != 0;
+ },
+
+ /**
+ * Returns the value of the spellchecking preference represented by UI,
+ * preserving the preference's "hidden" value if the preference is
+ * unchanged and represents a value not strictly allowed in UI.
+ */
+ writeCheckSpelling() {
+ var checkbox = document.getElementById("checkSpelling");
+ if (checkbox.checked) {
+ if (this._storedSpellCheck == 2) {
+ return 2;
+ }
+ return 1;
+ }
+ return 0;
+ },
+
+ updateDefaultPerformanceSettingsPref() {
+ let defaultPerformancePref = Preferences.get(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ );
+ let processCountPref = Preferences.get("dom.ipc.processCount");
+ let accelerationPref = Preferences.get("layers.acceleration.disabled");
+ if (
+ processCountPref.value != processCountPref.defaultValue ||
+ accelerationPref.value != accelerationPref.defaultValue
+ ) {
+ defaultPerformancePref.value = false;
+ }
+ },
+
+ updatePerformanceSettingsBox({ duringChangeEvent }) {
+ let defaultPerformancePref = Preferences.get(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ );
+ let performanceSettings = document.getElementById("performanceSettings");
+ let processCountPref = Preferences.get("dom.ipc.processCount");
+ if (defaultPerformancePref.value) {
+ let accelerationPref = Preferences.get("layers.acceleration.disabled");
+ // Unset the value so process count will be decided by the platform.
+ processCountPref.value = processCountPref.defaultValue;
+ accelerationPref.value = accelerationPref.defaultValue;
+ performanceSettings.hidden = true;
+ } else {
+ performanceSettings.hidden = false;
+ }
+ },
+
+ buildContentProcessCountMenuList() {
+ if (Services.appinfo.fissionAutostart) {
+ document.getElementById("limitContentProcess").hidden = true;
+ document.getElementById("contentProcessCount").hidden = true;
+ document.getElementById(
+ "contentProcessCountEnabledDescription"
+ ).hidden = true;
+ document.getElementById(
+ "contentProcessCountDisabledDescription"
+ ).hidden = true;
+ return;
+ }
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ let processCountPref = Preferences.get("dom.ipc.processCount");
+ let defaultProcessCount = processCountPref.defaultValue;
+
+ let contentProcessCount =
+ document.querySelector(`#contentProcessCount > menupopup >
+ menuitem[value="${defaultProcessCount}"]`);
+
+ document.l10n.setAttributes(
+ contentProcessCount,
+ "performance-default-content-process-count",
+ { num: defaultProcessCount }
+ );
+
+ document.getElementById("limitContentProcess").disabled = false;
+ document.getElementById("contentProcessCount").disabled = false;
+ document.getElementById(
+ "contentProcessCountEnabledDescription"
+ ).hidden = false;
+ document.getElementById(
+ "contentProcessCountDisabledDescription"
+ ).hidden = true;
+ } else {
+ document.getElementById("limitContentProcess").disabled = true;
+ document.getElementById("contentProcessCount").disabled = true;
+ document.getElementById(
+ "contentProcessCountEnabledDescription"
+ ).hidden = true;
+ document.getElementById(
+ "contentProcessCountDisabledDescription"
+ ).hidden = false;
+ }
+ },
+
+ _minUpdatePrefDisableTime: 1000,
+ /**
+ * Selects the correct item in the update radio group
+ */
+ async readUpdateAutoPref() {
+ if (
+ AppConstants.MOZ_UPDATER &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp
+ ) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+
+ radiogroup.disabled = true;
+ let enabled = await UpdateUtils.getAppUpdateAutoEnabled();
+ radiogroup.value = enabled;
+ radiogroup.disabled = false;
+
+ this.maybeDisableBackgroundUpdateControls();
+ }
+ },
+
+ /**
+ * Writes the value of the automatic update radio group to the disk
+ */
+ async writeUpdateAutoPref() {
+ if (
+ AppConstants.MOZ_UPDATER &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp
+ ) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+ let updateAutoValue = radiogroup.value == "true";
+ let _disableTimeOverPromise = new Promise(r =>
+ setTimeout(r, this._minUpdatePrefDisableTime)
+ );
+ radiogroup.disabled = true;
+ try {
+ await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue);
+ await _disableTimeOverPromise;
+ radiogroup.disabled = false;
+ } catch (error) {
+ console.error(error);
+ await Promise.all([
+ this.readUpdateAutoPref(),
+ this.reportUpdatePrefWriteError(),
+ ]);
+ return;
+ }
+
+ this.maybeDisableBackgroundUpdateControls();
+
+ // If the value was changed to false the user should be given the option
+ // to discard an update if there is one.
+ if (!updateAutoValue) {
+ await this.checkUpdateInProgress();
+ }
+ // For tests:
+ radiogroup.dispatchEvent(new CustomEvent("ProcessedUpdatePrefChange"));
+ }
+ },
+
+ isBackgroundUpdateUIAvailable() {
+ return (
+ AppConstants.MOZ_UPDATE_AGENT &&
+ // This UI controls a per-installation pref. It won't necessarily work
+ // properly if per-installation prefs aren't supported.
+ UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp &&
+ !UpdateUtils.appUpdateSettingIsLocked("app.update.background.enabled")
+ );
+ },
+
+ maybeDisableBackgroundUpdateControls() {
+ if (this.isBackgroundUpdateUIAvailable()) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+ let updateAutoEnabled = radiogroup.value == "true";
+
+ // This control is only active if auto update is enabled.
+ document.getElementById("backgroundUpdate").disabled = !updateAutoEnabled;
+ }
+ },
+
+ async readBackgroundUpdatePref() {
+ const prefName = "app.update.background.enabled";
+ if (this.isBackgroundUpdateUIAvailable()) {
+ let backgroundCheckbox = document.getElementById("backgroundUpdate");
+
+ // When the page first loads, the checkbox is unchecked until we finish
+ // reading the config file from the disk. But, ideally, we don't want to
+ // give the user the impression that this setting has somehow gotten
+ // turned off and they need to turn it back on. We also don't want the
+ // user interacting with the control, expecting a particular behavior, and
+ // then have the read complete and change the control in an unexpected
+ // way. So we disable the control while we are reading.
+ // The only entry points for this function are page load and user
+ // interaction with the control. By disabling the control to prevent
+ // further user interaction, we prevent the possibility of entering this
+ // function a second time while we are still reading.
+ backgroundCheckbox.disabled = true;
+
+ // If we haven't already done this, it might result in the effective value
+ // of the Background Update pref changing. Thus, we should do it before
+ // we tell the user what value this pref has.
+ await BackgroundUpdate.ensureExperimentToRolloutTransitionPerformed();
+
+ let enabled = await UpdateUtils.readUpdateConfigSetting(prefName);
+ backgroundCheckbox.checked = enabled;
+ this.maybeDisableBackgroundUpdateControls();
+ }
+ },
+
+ async writeBackgroundUpdatePref() {
+ const prefName = "app.update.background.enabled";
+ if (this.isBackgroundUpdateUIAvailable()) {
+ let backgroundCheckbox = document.getElementById("backgroundUpdate");
+ backgroundCheckbox.disabled = true;
+ let backgroundUpdateEnabled = backgroundCheckbox.checked;
+ try {
+ await UpdateUtils.writeUpdateConfigSetting(
+ prefName,
+ backgroundUpdateEnabled
+ );
+ } catch (error) {
+ console.error(error);
+ await this.readBackgroundUpdatePref();
+ await this.reportUpdatePrefWriteError();
+ return;
+ }
+
+ this.maybeDisableBackgroundUpdateControls();
+ }
+ },
+
+ async reportUpdatePrefWriteError() {
+ let [title, message] = await document.l10n.formatValues([
+ { id: "update-setting-write-failure-title2" },
+ {
+ id: "update-setting-write-failure-message2",
+ args: { path: UpdateUtils.configFilePath },
+ },
+ ]);
+
+ // Set up the Ok Button
+ let buttonFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK;
+ Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+ },
+
+ async checkUpdateInProgress() {
+ let um = Cc["@mozilla.org/updates/update-manager;1"].getService(
+ Ci.nsIUpdateManager
+ );
+ if (!um.readyUpdate && !um.downloadingUpdate) {
+ return;
+ }
+
+ let [title, message, okButton, cancelButton] =
+ await document.l10n.formatValues([
+ { id: "update-in-progress-title" },
+ { id: "update-in-progress-message" },
+ { id: "update-in-progress-ok-button" },
+ { id: "update-in-progress-cancel-button" },
+ ]);
+
+ // Continue is the cancel button which is BUTTON_POS_1 and is set as the
+ // default so pressing escape or using a platform standard method of closing
+ // the UI will not discard the update.
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 +
+ Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+
+ let rv = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ okButton,
+ cancelButton,
+ null,
+ null,
+ {}
+ );
+ if (rv != 1) {
+ let aus = Cc["@mozilla.org/updates/update-service;1"].getService(
+ Ci.nsIApplicationUpdateService
+ );
+ await aus.stopDownload();
+ um.cleanupReadyUpdate();
+ um.cleanupDownloadingUpdate();
+ }
+ },
+
+ /**
+ * Displays the history of installed updates.
+ */
+ showUpdates() {
+ gSubDialog.open("chrome://mozapps/content/update/history.xhtml");
+ },
+
+ destroy() {
+ window.removeEventListener("unload", this);
+ Services.prefs.removeObserver(PREF_CONTAINERS_EXTENSION, this);
+ Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
+ Services.obs.removeObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC);
+ AppearanceChooser.destroy();
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ // nsIObserver
+
+ async observe(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed") {
+ if (aData == PREF_CONTAINERS_EXTENSION) {
+ this.readBrowserContainersCheckbox();
+ return;
+ }
+ // Rebuild the list when there are changes to preferences that influence
+ // whether or not to show certain entries in the list.
+ if (!this._storingAction) {
+ await this._rebuildView();
+ }
+ } else if (aTopic == AUTO_UPDATE_CHANGED_TOPIC) {
+ if (!AppConstants.MOZ_UPDATER) {
+ return;
+ }
+ if (aData != "true" && aData != "false") {
+ throw new Error("Invalid preference value for app.update.auto");
+ }
+ document.getElementById("updateRadioGroup").value = aData;
+ this.maybeDisableBackgroundUpdateControls();
+ } else if (aTopic == BACKGROUND_UPDATE_CHANGED_TOPIC) {
+ if (!AppConstants.MOZ_UPDATE_AGENT) {
+ return;
+ }
+ if (aData != "true" && aData != "false") {
+ throw new Error(
+ "Invalid preference value for app.update.background.enabled"
+ );
+ }
+ document.getElementById("backgroundUpdate").checked = aData == "true";
+ }
+ },
+
+ // EventListener
+
+ handleEvent(aEvent) {
+ if (aEvent.type == "unload") {
+ this.destroy();
+ if (AppConstants.MOZ_UPDATER) {
+ onUnload();
+ }
+ }
+ },
+
+ // Composed Model Construction
+
+ _loadData() {
+ this._loadInternalHandlers();
+ this._loadApplicationHandlers();
+ },
+
+ /**
+ * Load higher level internal handlers so they can be turned on/off in the
+ * applications menu.
+ */
+ _loadInternalHandlers() {
+ let internalHandlers = [new PDFHandlerInfoWrapper()];
+
+ let enabledHandlers = Services.prefs
+ .getCharPref("browser.download.viewableInternally.enabledTypes", "")
+ .trim();
+ if (enabledHandlers) {
+ for (let ext of enabledHandlers.split(",")) {
+ internalHandlers.push(
+ new ViewableInternallyHandlerInfoWrapper(null, ext.trim())
+ );
+ }
+ }
+ for (let internalHandler of internalHandlers) {
+ if (internalHandler.enabled) {
+ this._handledTypes[internalHandler.type] = internalHandler;
+ }
+ }
+ },
+
+ /**
+ * Load the set of handlers defined by the application datastore.
+ */
+ _loadApplicationHandlers() {
+ for (let wrappedHandlerInfo of gHandlerService.enumerate()) {
+ let type = wrappedHandlerInfo.type;
+
+ let handlerInfoWrapper;
+ if (type in this._handledTypes) {
+ handlerInfoWrapper = this._handledTypes[type];
+ } else {
+ if (DownloadIntegration.shouldViewDownloadInternally(type)) {
+ handlerInfoWrapper = new ViewableInternallyHandlerInfoWrapper(type);
+ } else {
+ handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo);
+ }
+ this._handledTypes[type] = handlerInfoWrapper;
+ }
+ }
+ },
+
+ // View Construction
+
+ selectedHandlerListItem: null,
+
+ _initListEventHandlers() {
+ this._list.addEventListener("select", event => {
+ if (event.target != this._list) {
+ return;
+ }
+
+ let handlerListItem =
+ this._list.selectedItem &&
+ HandlerListItem.forNode(this._list.selectedItem);
+ if (this.selectedHandlerListItem == handlerListItem) {
+ return;
+ }
+
+ if (this.selectedHandlerListItem) {
+ this.selectedHandlerListItem.showActionsMenu = false;
+ }
+ this.selectedHandlerListItem = handlerListItem;
+ if (handlerListItem) {
+ this.rebuildActionsMenu();
+ handlerListItem.showActionsMenu = true;
+ }
+ });
+ },
+
+ async _rebuildVisibleTypes() {
+ this._visibleTypes = [];
+
+ // Map whose keys are string descriptions and values are references to the
+ // first visible HandlerInfoWrapper that has this description. We use this
+ // to determine whether or not to annotate descriptions with their types to
+ // distinguish duplicate descriptions from each other.
+ let visibleDescriptions = new Map();
+ for (let type in this._handledTypes) {
+ // Yield before processing each handler info object to avoid monopolizing
+ // the main thread, as the objects are retrieved lazily, and retrieval
+ // can be expensive on Windows.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ let handlerInfo = this._handledTypes[type];
+
+ // We couldn't find any reason to exclude the type, so include it.
+ this._visibleTypes.push(handlerInfo);
+
+ let key = JSON.stringify(handlerInfo.description);
+ let otherHandlerInfo = visibleDescriptions.get(key);
+ if (!otherHandlerInfo) {
+ // This is the first type with this description that we encountered
+ // while rebuilding the _visibleTypes array this time. Make sure the
+ // flag is reset so we won't add the type to the description.
+ handlerInfo.disambiguateDescription = false;
+ visibleDescriptions.set(key, handlerInfo);
+ } else {
+ // There is at least another type with this description. Make sure we
+ // add the type to the description on both HandlerInfoWrapper objects.
+ handlerInfo.disambiguateDescription = true;
+ otherHandlerInfo.disambiguateDescription = true;
+ }
+ }
+ },
+
+ async _rebuildView() {
+ let lastSelectedType =
+ this.selectedHandlerListItem &&
+ this.selectedHandlerListItem.handlerInfoWrapper.type;
+ this.selectedHandlerListItem = null;
+
+ // Clear the list of entries.
+ this._list.textContent = "";
+
+ var visibleTypes = this._visibleTypes;
+
+ let items = visibleTypes.map(
+ visibleType => new HandlerListItem(visibleType)
+ );
+ let itemsFragment = document.createDocumentFragment();
+ let lastSelectedItem;
+ for (let item of items) {
+ item.createNode(itemsFragment);
+ if (item.handlerInfoWrapper.type == lastSelectedType) {
+ lastSelectedItem = item;
+ }
+ }
+
+ for (let item of items) {
+ item.setupNode();
+ this.rebuildActionsMenu(item.node, item.handlerInfoWrapper);
+ item.refreshAction();
+ }
+
+ // If the user is filtering the list, then only show matching types.
+ // If we filter, we need to first localize the fragment, to
+ // be able to filter by localized values.
+ if (this._filter.value) {
+ await document.l10n.translateFragment(itemsFragment);
+
+ this._filterView(itemsFragment);
+
+ document.l10n.pauseObserving();
+ this._list.appendChild(itemsFragment);
+ document.l10n.resumeObserving();
+ } else {
+ // Otherwise we can just append the fragment and it'll
+ // get localized via the Mutation Observer.
+ this._list.appendChild(itemsFragment);
+ }
+
+ if (lastSelectedItem) {
+ this._list.selectedItem = lastSelectedItem.node;
+ }
+ },
+
+ /**
+ * Whether or not the given handler app is valid.
+ *
+ * @param aHandlerApp {nsIHandlerApp} the handler app in question
+ *
+ * @returns {boolean} whether or not it's valid
+ */
+ isValidHandlerApp(aHandlerApp) {
+ if (!aHandlerApp) {
+ return false;
+ }
+
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+ return this._isValidHandlerExecutable(aHandlerApp.executable);
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
+ return aHandlerApp.uriTemplate;
+ }
+
+ if (aHandlerApp instanceof Ci.nsIGIOMimeApp) {
+ return aHandlerApp.command;
+ }
+
+ return false;
+ },
+
+ _isValidHandlerExecutable(aExecutable) {
+ let leafName;
+ if (AppConstants.platform == "win") {
+ leafName = `${AppConstants.MOZ_APP_NAME}.exe`;
+ } else if (AppConstants.platform == "macosx") {
+ leafName = AppConstants.MOZ_MACBUNDLE_NAME;
+ } else {
+ leafName = `${AppConstants.MOZ_APP_NAME}-bin`;
+ }
+ return (
+ aExecutable &&
+ aExecutable.exists() &&
+ aExecutable.isExecutable() &&
+ // XXXben - we need to compare this with the running instance executable
+ // just don't know how to do that via script...
+ // XXXmano TBD: can probably add this to nsIShellService
+ aExecutable.leafName != leafName
+ );
+ },
+
+ /**
+ * Rebuild the actions menu for the selected entry. Gets called by
+ * the richlistitem constructor when an entry in the list gets selected.
+ */
+ rebuildActionsMenu(
+ typeItem = this._list.selectedItem,
+ handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper
+ ) {
+ var menu = typeItem.querySelector(".actionsMenu");
+ var menuPopup = menu.menupopup;
+
+ // Clear out existing items.
+ while (menuPopup.hasChildNodes()) {
+ menuPopup.removeChild(menuPopup.lastChild);
+ }
+
+ let internalMenuItem;
+ // Add the "Open in Firefox" option for optional internal handlers.
+ if (
+ handlerInfo instanceof InternalHandlerInfoWrapper &&
+ !handlerInfo.preventInternalViewing
+ ) {
+ internalMenuItem = document.createXULElement("menuitem");
+ internalMenuItem.setAttribute(
+ "action",
+ Ci.nsIHandlerInfo.handleInternally
+ );
+ document.l10n.setAttributes(internalMenuItem, "applications-open-inapp");
+ internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "handleInternally");
+ menuPopup.appendChild(internalMenuItem);
+ }
+
+ var askMenuItem = document.createXULElement("menuitem");
+ askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk);
+ document.l10n.setAttributes(askMenuItem, "applications-always-ask");
+ askMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask");
+ menuPopup.appendChild(askMenuItem);
+
+ // Create a menu item for saving to disk.
+ // Note: this option isn't available to protocol types, since we don't know
+ // what it means to save a URL having a certain scheme to disk.
+ if (handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ var saveMenuItem = document.createXULElement("menuitem");
+ saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk);
+ document.l10n.setAttributes(saveMenuItem, "applications-action-save");
+ saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save");
+ menuPopup.appendChild(saveMenuItem);
+ }
+
+ // Add a separator to distinguish these items from the helper app items
+ // that follow them.
+ let menuseparator = document.createXULElement("menuseparator");
+ menuPopup.appendChild(menuseparator);
+
+ // Create a menu item for the OS default application, if any.
+ if (handlerInfo.hasDefaultHandler) {
+ var defaultMenuItem = document.createXULElement("menuitem");
+ defaultMenuItem.setAttribute(
+ "action",
+ Ci.nsIHandlerInfo.useSystemDefault
+ );
+ // If an internal option is available, don't show the application
+ // name for the OS default to prevent two options from appearing
+ // that may both say "Firefox".
+ if (internalMenuItem) {
+ document.l10n.setAttributes(
+ defaultMenuItem,
+ "applications-use-os-default"
+ );
+ defaultMenuItem.setAttribute("image", ICON_URL_APP);
+ } else {
+ document.l10n.setAttributes(
+ defaultMenuItem,
+ "applications-use-app-default",
+ {
+ "app-name": handlerInfo.defaultDescription,
+ }
+ );
+ defaultMenuItem.setAttribute(
+ "image",
+ handlerInfo.iconURLForSystemDefault
+ );
+ }
+
+ menuPopup.appendChild(defaultMenuItem);
+ }
+
+ // Create menu items for possible handlers.
+ let preferredApp = handlerInfo.preferredApplicationHandler;
+ var possibleAppMenuItems = [];
+ for (let possibleApp of handlerInfo.possibleApplicationHandlers.enumerate()) {
+ if (!this.isValidHandlerApp(possibleApp)) {
+ continue;
+ }
+
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
+ let label;
+ if (possibleApp instanceof Ci.nsILocalHandlerApp) {
+ label = getFileDisplayName(possibleApp.executable);
+ } else {
+ label = possibleApp.name;
+ }
+ document.l10n.setAttributes(menuItem, "applications-use-app", {
+ "app-name": label,
+ });
+ menuItem.setAttribute(
+ "image",
+ this._getIconURLForHandlerApp(possibleApp)
+ );
+
+ // Attach the handler app object to the menu item so we can use it
+ // to make changes to the datastore when the user selects the item.
+ menuItem.handlerApp = possibleApp;
+
+ menuPopup.appendChild(menuItem);
+ possibleAppMenuItems.push(menuItem);
+ }
+ // Add gio handlers
+ if (gGIOService) {
+ var gioApps = gGIOService.getAppsForURIScheme(handlerInfo.type);
+ let possibleHandlers = handlerInfo.possibleApplicationHandlers;
+ for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) {
+ // OS handler share the same name, it's most likely the same app, skipping...
+ if (handler.name == handlerInfo.defaultDescription) {
+ continue;
+ }
+ // Check if the handler is already in possibleHandlers
+ let appAlreadyInHandlers = false;
+ for (let i = possibleHandlers.length - 1; i >= 0; --i) {
+ let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
+ // nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp
+ if (handler.equals(app)) {
+ appAlreadyInHandlers = true;
+ break;
+ }
+ }
+ if (!appAlreadyInHandlers) {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
+ document.l10n.setAttributes(menuItem, "applications-use-app", {
+ "app-name": handler.name,
+ });
+ menuItem.setAttribute(
+ "image",
+ this._getIconURLForHandlerApp(handler)
+ );
+
+ // Attach the handler app object to the menu item so we can use it
+ // to make changes to the datastore when the user selects the item.
+ menuItem.handlerApp = handler;
+
+ menuPopup.appendChild(menuItem);
+ possibleAppMenuItems.push(menuItem);
+ }
+ }
+ }
+
+ // Create a menu item for selecting a local application.
+ let canOpenWithOtherApp = true;
+ if (AppConstants.platform == "win") {
+ // On Windows, selecting an application to open another application
+ // would be meaningless so we special case executables.
+ let executableType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromExtension("exe");
+ canOpenWithOtherApp = handlerInfo.type != executableType;
+ }
+ if (canOpenWithOtherApp) {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.className = "choose-app-item";
+ menuItem.addEventListener("command", function (e) {
+ gMainPane.chooseApp(e);
+ });
+ document.l10n.setAttributes(menuItem, "applications-use-other");
+ menuPopup.appendChild(menuItem);
+ }
+
+ // Create a menu item for managing applications.
+ if (possibleAppMenuItems.length) {
+ let menuItem = document.createXULElement("menuseparator");
+ menuPopup.appendChild(menuItem);
+ menuItem = document.createXULElement("menuitem");
+ menuItem.className = "manage-app-item";
+ menuItem.addEventListener("command", function (e) {
+ gMainPane.manageApp(e);
+ });
+ document.l10n.setAttributes(menuItem, "applications-manage-app");
+ menuPopup.appendChild(menuItem);
+ }
+
+ // Select the item corresponding to the preferred action. If the always
+ // ask flag is set, it overrides the preferred action. Otherwise we pick
+ // the item identified by the preferred action (when the preferred action
+ // is to use a helper app, we have to pick the specific helper app item).
+ if (handlerInfo.alwaysAskBeforeHandling) {
+ menu.selectedItem = askMenuItem;
+ } else {
+ // The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify
+ // the actions the application can take with content of various types.
+ // But since we've stopped support for plugins, there's no value
+ // identifying the "use plugin" action, so we use this constant instead.
+ const kActionUsePlugin = 5;
+
+ switch (handlerInfo.preferredAction) {
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (internalMenuItem) {
+ menu.selectedItem = internalMenuItem;
+ } else {
+ console.error("No menu item defined to set!");
+ }
+ break;
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ // We might not have a default item if we're not aware of an
+ // OS-default handler for this type:
+ menu.selectedItem = defaultMenuItem || askMenuItem;
+ break;
+ case Ci.nsIHandlerInfo.useHelperApp:
+ if (preferredApp) {
+ let preferredItem = possibleAppMenuItems.find(v =>
+ v.handlerApp.equals(preferredApp)
+ );
+ if (preferredItem) {
+ menu.selectedItem = preferredItem;
+ } else {
+ // This shouldn't happen, but let's make sure we end up with a
+ // selected item:
+ let possible = possibleAppMenuItems
+ .map(v => v.handlerApp && v.handlerApp.name)
+ .join(", ");
+ console.error(
+ new Error(
+ `Preferred handler for ${handlerInfo.type} not in list of possible handlers!? (List: ${possible})`
+ )
+ );
+ menu.selectedItem = askMenuItem;
+ }
+ }
+ break;
+ case kActionUsePlugin:
+ // We no longer support plugins, select "ask" instead:
+ menu.selectedItem = askMenuItem;
+ break;
+ case Ci.nsIHandlerInfo.saveToDisk:
+ menu.selectedItem = saveMenuItem;
+ break;
+ }
+ }
+ },
+
+ // Sorting & Filtering
+
+ _sortColumn: null,
+
+ /**
+ * Sort the list when the user clicks on a column header.
+ */
+ sort(event) {
+ if (event.button != 0) {
+ return;
+ }
+ var column = event.target;
+
+ // If the user clicked on a new sort column, remove the direction indicator
+ // from the old column.
+ if (this._sortColumn && this._sortColumn != column) {
+ this._sortColumn.removeAttribute("sortDirection");
+ }
+
+ this._sortColumn = column;
+
+ // Set (or switch) the sort direction indicator.
+ if (column.getAttribute("sortDirection") == "ascending") {
+ column.setAttribute("sortDirection", "descending");
+ } else {
+ column.setAttribute("sortDirection", "ascending");
+ }
+
+ this._sortListView();
+ },
+
+ async _sortListView() {
+ if (!this._sortColumn) {
+ return;
+ }
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ await document.l10n.translateFragment(this._list);
+ let items = Array.from(this._list.children);
+
+ let textForNode;
+ if (this._sortColumn.getAttribute("value") === "type") {
+ textForNode = n => n.querySelector(".typeDescription").textContent;
+ } else {
+ textForNode = n => n.querySelector(".actionsMenu").getAttribute("label");
+ }
+
+ let sortDir = this._sortColumn.getAttribute("sortDirection");
+ let multiplier = sortDir == "descending" ? -1 : 1;
+ items.sort(
+ (a, b) => multiplier * comp.compare(textForNode(a), textForNode(b))
+ );
+
+ // Re-append items in the correct order:
+ items.forEach(item => this._list.appendChild(item));
+ },
+
+ _filterView(frag = this._list) {
+ const filterValue = this._filter.value.toLowerCase();
+ for (let elem of frag.children) {
+ const typeDescription =
+ elem.querySelector(".typeDescription").textContent;
+ const actionDescription = elem
+ .querySelector(".actionDescription")
+ .getAttribute("value");
+ elem.hidden =
+ !typeDescription.toLowerCase().includes(filterValue) &&
+ !actionDescription.toLowerCase().includes(filterValue);
+ }
+ },
+
+ /**
+ * Filter the list when the user enters a filter term into the filter field.
+ */
+ filter() {
+ this._rebuildView(); // FIXME: Should this be await since bug 1508156?
+ },
+
+ focusFilterBox() {
+ this._filter.focus();
+ this._filter.select();
+ },
+
+ // Changes
+
+ // Whether or not we are currently storing the action selected by the user.
+ // We use this to suppress notification-triggered updates to the list when
+ // we make changes that may spawn such updates.
+ // XXXgijs: this was definitely necessary when we changed feed preferences
+ // from within _storeAction and its calltree. Now, it may still be
+ // necessary, to avoid calling _rebuildView. bug 1499350 has more details.
+ _storingAction: false,
+
+ onSelectAction(aActionItem) {
+ this._storingAction = true;
+
+ try {
+ this._storeAction(aActionItem);
+ } finally {
+ this._storingAction = false;
+ }
+ },
+
+ _storeAction(aActionItem) {
+ var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
+
+ let action = parseInt(aActionItem.getAttribute("action"));
+
+ // Set the preferred application handler.
+ // We leave the existing preferred app in the list when we set
+ // the preferred action to something other than useHelperApp so that
+ // legacy datastores that don't have the preferred app in the list
+ // of possible apps still include the preferred app in the list of apps
+ // the user can choose to handle the type.
+ if (action == Ci.nsIHandlerInfo.useHelperApp) {
+ handlerInfo.preferredApplicationHandler = aActionItem.handlerApp;
+ }
+
+ // Set the "always ask" flag.
+ if (action == Ci.nsIHandlerInfo.alwaysAsk) {
+ handlerInfo.alwaysAskBeforeHandling = true;
+ } else {
+ handlerInfo.alwaysAskBeforeHandling = false;
+ }
+
+ // Set the preferred action.
+ handlerInfo.preferredAction = action;
+
+ handlerInfo.store();
+
+ // Update the action label and image to reflect the new preferred action.
+ this.selectedHandlerListItem.refreshAction();
+ },
+
+ manageApp(aEvent) {
+ // Don't let the normal "on select action" handler get this event,
+ // as we handle it specially ourselves.
+ aEvent.stopPropagation();
+
+ var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
+
+ let onComplete = () => {
+ // Rebuild the actions menu so that we revert to the previous selection,
+ // or "Always ask" if the previous default application has been removed
+ this.rebuildActionsMenu();
+
+ // update the richlistitem too. Will be visible when selecting another row
+ this.selectedHandlerListItem.refreshAction();
+ };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/applicationManager.xhtml",
+ { features: "resizable=no", closingCallback: onComplete },
+ handlerInfo
+ );
+ },
+
+ async chooseApp(aEvent) {
+ // Don't let the normal "on select action" handler get this event,
+ // as we handle it specially ourselves.
+ aEvent.stopPropagation();
+
+ var handlerApp;
+ let chooseAppCallback = aHandlerApp => {
+ // Rebuild the actions menu whether the user picked an app or canceled.
+ // If they picked an app, we want to add the app to the menu and select it.
+ // If they canceled, we want to go back to their previous selection.
+ this.rebuildActionsMenu();
+
+ // If the user picked a new app from the menu, select it.
+ if (aHandlerApp) {
+ let typeItem = this._list.selectedItem;
+ let actionsMenu = typeItem.querySelector(".actionsMenu");
+ let menuItems = actionsMenu.menupopup.childNodes;
+ for (let i = 0; i < menuItems.length; i++) {
+ let menuItem = menuItems[i];
+ if (menuItem.handlerApp && menuItem.handlerApp.equals(aHandlerApp)) {
+ actionsMenu.selectedIndex = i;
+ this.onSelectAction(menuItem);
+ break;
+ }
+ }
+ }
+ };
+
+ if (AppConstants.platform == "win") {
+ var params = {};
+ var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
+
+ params.mimeInfo = handlerInfo.wrappedHandlerInfo;
+ params.title = await document.l10n.formatValue(
+ "applications-select-helper"
+ );
+ if ("id" in handlerInfo.description) {
+ params.description = await document.l10n.formatValue(
+ handlerInfo.description.id,
+ handlerInfo.description.args
+ );
+ } else {
+ params.description = handlerInfo.typeDescription.raw;
+ }
+ params.filename = null;
+ params.handlerApp = null;
+
+ let onAppSelected = () => {
+ if (this.isValidHandlerApp(params.handlerApp)) {
+ handlerApp = params.handlerApp;
+
+ // Add the app to the type's list of possible handlers.
+ handlerInfo.addPossibleApplicationHandler(handlerApp);
+ }
+
+ chooseAppCallback(handlerApp);
+ };
+
+ gSubDialog.open(
+ "chrome://global/content/appPicker.xhtml",
+ { closingCallback: onAppSelected },
+ params
+ );
+ } else {
+ let winTitle = await document.l10n.formatValue(
+ "applications-select-helper"
+ );
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = aResult => {
+ if (
+ aResult == Ci.nsIFilePicker.returnOK &&
+ fp.file &&
+ this._isValidHandlerExecutable(fp.file)
+ ) {
+ handlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ handlerApp.name = getFileDisplayName(fp.file);
+ handlerApp.executable = fp.file;
+
+ // Add the app to the type's list of possible handlers.
+ let handler = this.selectedHandlerListItem.handlerInfoWrapper;
+ handler.addPossibleApplicationHandler(handlerApp);
+
+ chooseAppCallback(handlerApp);
+ }
+ };
+
+ // Prompt the user to pick an app. If they pick one, and it's a valid
+ // selection, then add it to the list of possible handlers.
+ fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+ fp.open(fpCallback);
+ }
+ },
+
+ _getIconURLForHandlerApp(aHandlerApp) {
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+ return this._getIconURLForFile(aHandlerApp.executable);
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
+ return this._getIconURLForWebApp(aHandlerApp.uriTemplate);
+ }
+
+ // We know nothing about other kinds of handler apps.
+ return "";
+ },
+
+ _getIconURLForFile(aFile) {
+ var fph = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ var urlSpec = fph.getURLSpecFromActualFile(aFile);
+
+ return "moz-icon://" + urlSpec + "?size=16";
+ },
+
+ _getIconURLForWebApp(aWebAppURITemplate) {
+ var uri = Services.io.newURI(aWebAppURITemplate);
+
+ // Unfortunately we can't use the favicon service to get the favicon,
+ // because the service looks in the annotations table for a record with
+ // the exact URL we give it, and users won't have such records for URLs
+ // they don't visit, and users won't visit the web app's URL template,
+ // they'll only visit URLs derived from that template (i.e. with %s
+ // in the template replaced by the URL of the content being handled).
+
+ if (
+ /^https?$/.test(uri.scheme) &&
+ Services.prefs.getBoolPref("browser.chrome.site_icons")
+ ) {
+ return uri.prePath + "/favicon.ico";
+ }
+
+ return "";
+ },
+
+ // DOWNLOADS
+
+ /*
+ * Preferences:
+ *
+ * browser.download.useDownloadDir - bool
+ * True - Save files directly to the folder configured via the
+ * browser.download.folderList preference.
+ * False - Always ask the user where to save a file and default to
+ * browser.download.lastDir when displaying a folder picker dialog.
+ * browser.download.always_ask_before_handling_new_types - bool
+ * Defines the default behavior for new file handlers.
+ * True - When downloading a file that doesn't match any existing
+ * handlers, ask the user whether to save or open the file.
+ * False - Save the file. The user can change the default action in
+ * the Applications section in the preferences UI.
+ * browser.download.dir - local file handle
+ * A local folder the user may have selected for downloaded files to be
+ * saved. Migration of other browser settings may also set this path.
+ * This folder is enabled when folderList equals 2.
+ * browser.download.lastDir - local file handle
+ * May contain the last folder path accessed when the user browsed
+ * via the file save-as dialog. (see contentAreaUtils.js)
+ * browser.download.folderList - int
+ * Indicates the location users wish to save downloaded files too.
+ * It is also used to display special file labels when the default
+ * download location is either the Desktop or the Downloads folder.
+ * Values:
+ * 0 - The desktop is the default download location.
+ * 1 - The system's downloads folder is the default download location.
+ * 2 - The default download location is elsewhere as specified in
+ * browser.download.dir.
+ * browser.download.downloadDir
+ * deprecated.
+ * browser.download.defaultFolder
+ * deprecated.
+ */
+
+ /**
+ * Disables the downloads folder field and Browse button if the default
+ * download directory pref is locked (e.g., by the DownloadDirectory or
+ * DefaultDownloadDirectory policies)
+ */
+ readUseDownloadDir() {
+ document.getElementById("downloadFolder").disabled =
+ document.getElementById("chooseFolder").disabled =
+ document.getElementById("saveTo").disabled =
+ Preferences.get("browser.download.dir").locked ||
+ Preferences.get("browser.download.folderList").locked;
+ // don't override the preference's value in UI
+ return undefined;
+ },
+
+ /**
+ * Displays a file picker in which the user can choose the location where
+ * downloads are automatically saved, updating preferences and UI in
+ * response to the choice, if one is made.
+ */
+ chooseFolder() {
+ return this.chooseFolderTask().catch(console.error);
+ },
+ async chooseFolderTask() {
+ let [title] = await document.l10n.formatValues([
+ { id: "choose-download-folder-title" },
+ ]);
+ let folderListPref = Preferences.get("browser.download.folderList");
+ let currentDirPref = await this._indexToFolder(folderListPref.value);
+ let defDownloads = await this._indexToFolder(1);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.init(window, title, Ci.nsIFilePicker.modeGetFolder);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ // First try to open what's currently configured
+ if (currentDirPref && currentDirPref.exists()) {
+ fp.displayDirectory = currentDirPref;
+ } else if (defDownloads && defDownloads.exists()) {
+ // Try the system's download dir
+ fp.displayDirectory = defDownloads;
+ } else {
+ // Fall back to Desktop
+ fp.displayDirectory = await this._indexToFolder(0);
+ }
+
+ let result = await new Promise(resolve => fp.open(resolve));
+ if (result != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ let downloadDirPref = Preferences.get("browser.download.dir");
+ downloadDirPref.value = fp.file;
+ folderListPref.value = await this._folderToIndex(fp.file);
+ // Note, the real prefs will not be updated yet, so dnld manager's
+ // userDownloadsDirectory may not return the right folder after
+ // this code executes. displayDownloadDirPref will be called on
+ // the assignment above to update the UI.
+ },
+
+ /**
+ * Initializes the download folder display settings based on the user's
+ * preferences.
+ */
+ displayDownloadDirPref() {
+ this.displayDownloadDirPrefTask().catch(console.error);
+
+ // don't override the preference's value in UI
+ return undefined;
+ },
+
+ async displayDownloadDirPrefTask() {
+ // We're async for localization reasons, and we can get called several
+ // times in the same turn of the event loop (!) because of how the
+ // preferences bindings work... but the speed of localization
+ // shouldn't impact what gets displayed to the user in the end - the
+ // last call should always win.
+ // To accomplish this, store a unique object when we enter this function,
+ // and if by the end of the function that stored object has been
+ // overwritten, don't update the UI but leave it to the last
+ // caller to this function to do.
+ let token = {};
+ this._downloadDisplayToken = token;
+
+ var downloadFolder = document.getElementById("downloadFolder");
+
+ let folderIndex = Preferences.get("browser.download.folderList").value;
+ // For legacy users using cloudstorage pref with folderIndex as 3 (See bug 1751093),
+ // compute folderIndex using the current directory pref
+ if (folderIndex == 3) {
+ let currentDirPref = Preferences.get("browser.download.dir");
+ folderIndex = currentDirPref.value
+ ? await this._folderToIndex(currentDirPref.value)
+ : 1;
+ }
+
+ // Display a 'pretty' label or the path in the UI.
+ let { folderDisplayName, file } =
+ await this._getSystemDownloadFolderDetails(folderIndex);
+ // Figure out an icon url:
+ let fph = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ let iconUrlSpec = fph.getURLSpecFromDir(file);
+
+ // Ensure that the last entry to this function always wins
+ // (see comment at the start of this method):
+ if (this._downloadDisplayToken != token) {
+ return;
+ }
+ // note: downloadFolder.value is not read elsewhere in the code, its only purpose is to display to the user
+ downloadFolder.value = folderDisplayName;
+ downloadFolder.style.backgroundImage =
+ "url(moz-icon://" + iconUrlSpec + "?size=16)";
+ },
+
+ async _getSystemDownloadFolderDetails(folderIndex) {
+ let downloadsDir = await this._getDownloadsFolder("Downloads");
+ let desktopDir = await this._getDownloadsFolder("Desktop");
+ let currentDirPref = Preferences.get("browser.download.dir");
+
+ let file;
+ let firefoxLocalizedName;
+ if (folderIndex == 2 && currentDirPref.value) {
+ file = currentDirPref.value;
+ if (file.equals(downloadsDir)) {
+ folderIndex = 1;
+ } else if (file.equals(desktopDir)) {
+ folderIndex = 0;
+ }
+ }
+ switch (folderIndex) {
+ case 2: // custom path, handled above.
+ break;
+
+ case 1: {
+ // downloads
+ file = downloadsDir;
+ firefoxLocalizedName = await document.l10n.formatValues([
+ { id: "downloads-folder-name" },
+ ]);
+ break;
+ }
+
+ case 0:
+ // fall through
+ default: {
+ file = desktopDir;
+ firefoxLocalizedName = await document.l10n.formatValues([
+ { id: "desktop-folder-name" },
+ ]);
+ }
+ }
+ if (firefoxLocalizedName) {
+ let folderDisplayName, leafName;
+ // Either/both of these can throw, so check for failures in both cases
+ // so we don't just break display of the download pref:
+ try {
+ folderDisplayName = file.displayName;
+ } catch (ex) {
+ /* ignored */
+ }
+ try {
+ leafName = file.leafName;
+ } catch (ex) {
+ /* ignored */
+ }
+
+ // If we found a localized name that's different from the leaf name,
+ // use that:
+ if (folderDisplayName && folderDisplayName != leafName) {
+ return { file, folderDisplayName };
+ }
+
+ // Otherwise, check if we've got a localized name ourselves.
+ if (firefoxLocalizedName) {
+ // You can't move the system download or desktop dir on macOS,
+ // so if those are in use just display them. On other platforms
+ // only do so if the folder matches the localized name.
+ if (
+ AppConstants.platform == "mac" ||
+ leafName == firefoxLocalizedName
+ ) {
+ return { file, folderDisplayName: firefoxLocalizedName };
+ }
+ }
+ }
+ // If we get here, attempts to use a "pretty" name failed. Just display
+ // the full path:
+ if (file) {
+ // Force the left-to-right direction when displaying a custom path.
+ return { file, folderDisplayName: `\u2066${file.path}\u2069` };
+ }
+ // Don't even have a file - fall back to desktop directory for the
+ // use of the icon, and an empty label:
+ file = desktopDir;
+ return { file, folderDisplayName: "" };
+ },
+
+ /**
+ * Returns the Downloads folder. If aFolder is "Desktop", then the Downloads
+ * folder returned is the desktop folder; otherwise, it is a folder whose name
+ * indicates that it is a download folder and whose path is as determined by
+ * the XPCOM directory service via the download manager's attribute
+ * defaultDownloadsDirectory.
+ *
+ * @throws if aFolder is not "Desktop" or "Downloads"
+ */
+ async _getDownloadsFolder(aFolder) {
+ switch (aFolder) {
+ case "Desktop":
+ return Services.dirsvc.get("Desk", Ci.nsIFile);
+ case "Downloads":
+ let downloadsDir = await Downloads.getSystemDownloadsDirectory();
+ return new FileUtils.File(downloadsDir);
+ }
+ throw new Error(
+ "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'"
+ );
+ },
+
+ /**
+ * Determines the type of the given folder.
+ *
+ * @param aFolder
+ * the folder whose type is to be determined
+ * @returns integer
+ * 0 if aFolder is the Desktop or is unspecified,
+ * 1 if aFolder is the Downloads folder,
+ * 2 otherwise
+ */
+ async _folderToIndex(aFolder) {
+ if (!aFolder || aFolder.equals(await this._getDownloadsFolder("Desktop"))) {
+ return 0;
+ } else if (aFolder.equals(await this._getDownloadsFolder("Downloads"))) {
+ return 1;
+ }
+ return 2;
+ },
+
+ /**
+ * Converts an integer into the corresponding folder.
+ *
+ * @param aIndex
+ * an integer
+ * @returns the Desktop folder if aIndex == 0,
+ * the Downloads folder if aIndex == 1,
+ * the folder stored in browser.download.dir
+ */
+ _indexToFolder(aIndex) {
+ switch (aIndex) {
+ case 0:
+ return this._getDownloadsFolder("Desktop");
+ case 1:
+ return this._getDownloadsFolder("Downloads");
+ }
+ var currentDirPref = Preferences.get("browser.download.dir");
+ return currentDirPref.value;
+ },
+};
+
+gMainPane.initialized = new Promise(res => {
+ gMainPane.setInitialized = res;
+});
+
+// Utilities
+
+function getFileDisplayName(file) {
+ if (AppConstants.platform == "win") {
+ if (file instanceof Ci.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ }
+ }
+ if (AppConstants.platform == "macosx") {
+ if (file instanceof Ci.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+ }
+ return file.leafName;
+}
+
+function getLocalHandlerApp(aFile) {
+ var localHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.name = getFileDisplayName(aFile);
+ localHandlerApp.executable = aFile;
+
+ return localHandlerApp;
+}
+
+// eslint-disable-next-line no-undef
+let gHandlerListItemFragment = MozXULElement.parseXULToFragment(`
+ <richlistitem>
+ <hbox class="typeContainer" flex="1" align="center">
+ <image class="typeIcon" width="16" height="16"
+ src="moz-icon://goat?size=16"/>
+ <label class="typeDescription" flex="1" crop="end"/>
+ </hbox>
+ <hbox class="actionContainer" flex="1" align="center">
+ <image class="actionIcon" width="16" height="16"/>
+ <label class="actionDescription" flex="1" crop="end"/>
+ </hbox>
+ <hbox class="actionsMenuContainer" flex="1">
+ <menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1">
+ <menupopup/>
+ </menulist>
+ </hbox>
+ </richlistitem>
+`);
+
+/**
+ * This is associated to <richlistitem> elements in the handlers view.
+ */
+class HandlerListItem {
+ static forNode(node) {
+ return gNodeToObjectMap.get(node);
+ }
+
+ constructor(handlerInfoWrapper) {
+ this.handlerInfoWrapper = handlerInfoWrapper;
+ }
+
+ setOrRemoveAttributes(iterable) {
+ for (let [selector, name, value] of iterable) {
+ let node = selector ? this.node.querySelector(selector) : this.node;
+ if (value) {
+ node.setAttribute(name, value);
+ } else {
+ node.removeAttribute(name);
+ }
+ }
+ }
+
+ createNode(list) {
+ list.appendChild(document.importNode(gHandlerListItemFragment, true));
+ this.node = list.lastChild;
+ gNodeToObjectMap.set(this.node, this);
+ }
+
+ setupNode() {
+ this.node
+ .querySelector(".actionsMenu")
+ .addEventListener("command", event =>
+ gMainPane.onSelectAction(event.originalTarget)
+ );
+
+ let typeDescription = this.handlerInfoWrapper.typeDescription;
+ this.setOrRemoveAttributes([
+ [null, "type", this.handlerInfoWrapper.type],
+ [".typeIcon", "src", this.handlerInfoWrapper.smallIcon],
+ ]);
+ localizeElement(
+ this.node.querySelector(".typeDescription"),
+ typeDescription
+ );
+ this.showActionsMenu = false;
+ }
+
+ refreshAction() {
+ let { actionIconClass } = this.handlerInfoWrapper;
+ this.setOrRemoveAttributes([
+ [null, APP_ICON_ATTR_NAME, actionIconClass],
+ [
+ ".actionIcon",
+ "src",
+ actionIconClass ? null : this.handlerInfoWrapper.actionIcon,
+ ],
+ ]);
+ const selectedItem = this.node.querySelector("[selected=true]");
+ if (!selectedItem) {
+ console.error("No selected item for " + this.handlerInfoWrapper.type);
+ return;
+ }
+ const { id, args } = document.l10n.getAttributes(selectedItem);
+ localizeElement(this.node.querySelector(".actionDescription"), {
+ id: id + "-label",
+ args,
+ });
+ localizeElement(this.node.querySelector(".actionsMenu"), { id, args });
+ }
+
+ set showActionsMenu(value) {
+ this.setOrRemoveAttributes([
+ [".actionContainer", "hidden", value],
+ [".actionsMenuContainer", "hidden", !value],
+ ]);
+ }
+}
+
+/**
+ * This API facilitates dual-model of some localization APIs which
+ * may operate on raw strings of l10n id/args pairs.
+ *
+ * The l10n can be:
+ *
+ * {raw: string} - raw strings to be used as text value of the element
+ * {id: string} - l10n-id
+ * {id: string, args: object} - l10n-id + l10n-args
+ */
+function localizeElement(node, l10n) {
+ if (l10n.hasOwnProperty("raw")) {
+ node.removeAttribute("data-l10n-id");
+ node.textContent = l10n.raw;
+ } else {
+ document.l10n.setAttributes(node, l10n.id, l10n.args);
+ }
+}
+
+/**
+ * This object wraps nsIHandlerInfo with some additional functionality
+ * the Applications prefpane needs to display and allow modification of
+ * the list of handled types.
+ *
+ * We create an instance of this wrapper for each entry we might display
+ * in the prefpane, and we compose the instances from various sources,
+ * including the handler service.
+ *
+ * We don't implement all the original nsIHandlerInfo functionality,
+ * just the stuff that the prefpane needs.
+ */
+class HandlerInfoWrapper {
+ constructor(type, handlerInfo) {
+ this.type = type;
+ this.wrappedHandlerInfo = handlerInfo;
+ this.disambiguateDescription = false;
+ }
+
+ get description() {
+ if (this.wrappedHandlerInfo.description) {
+ return { raw: this.wrappedHandlerInfo.description };
+ }
+
+ if (this.primaryExtension) {
+ var extension = this.primaryExtension.toUpperCase();
+ return { id: "applications-file-ending", args: { extension } };
+ }
+
+ return { raw: this.type };
+ }
+
+ /**
+ * Describe, in a human-readable fashion, the type represented by the given
+ * handler info object. Normally this is just the description, but if more
+ * than one object presents the same description, "disambiguateDescription"
+ * is set and we annotate the duplicate descriptions with the type itself
+ * to help users distinguish between those types.
+ */
+ get typeDescription() {
+ if (this.disambiguateDescription) {
+ const description = this.description;
+ if (description.id) {
+ // Pass through the arguments:
+ let { args = {} } = description;
+ args.type = this.type;
+ return {
+ id: description.id + "-with-type",
+ args,
+ };
+ }
+
+ return {
+ id: "applications-type-description-with-type",
+ args: {
+ "type-description": description.raw,
+ type: this.type,
+ },
+ };
+ }
+
+ return this.description;
+ }
+
+ get actionIconClass() {
+ if (this.alwaysAskBeforeHandling) {
+ return "ask";
+ }
+
+ switch (this.preferredAction) {
+ case Ci.nsIHandlerInfo.saveToDisk:
+ return "save";
+
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (this instanceof InternalHandlerInfoWrapper) {
+ return "handleInternally";
+ }
+ break;
+ }
+
+ return "";
+ }
+
+ get actionIcon() {
+ switch (this.preferredAction) {
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ return this.iconURLForSystemDefault;
+
+ case Ci.nsIHandlerInfo.useHelperApp:
+ let preferredApp = this.preferredApplicationHandler;
+ if (gMainPane.isValidHandlerApp(preferredApp)) {
+ return gMainPane._getIconURLForHandlerApp(preferredApp);
+ }
+
+ // This should never happen, but if preferredAction is set to some weird
+ // value, then fall back to the generic application icon.
+ // Explicit fall-through
+ default:
+ return ICON_URL_APP;
+ }
+ }
+
+ get iconURLForSystemDefault() {
+ // Handler info objects for MIME types on some OSes implement a property bag
+ // interface from which we can get an icon for the default app, so if we're
+ // dealing with a MIME type on one of those OSes, then try to get the icon.
+ if (
+ this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ this.wrappedHandlerInfo instanceof Ci.nsIPropertyBag
+ ) {
+ try {
+ let url = this.wrappedHandlerInfo.getProperty(
+ "defaultApplicationIconURL"
+ );
+ if (url) {
+ return url + "?size=16";
+ }
+ } catch (ex) {}
+ }
+
+ // If this isn't a MIME type object on an OS that supports retrieving
+ // the icon, or if we couldn't retrieve the icon for some other reason,
+ // then use a generic icon.
+ return ICON_URL_APP;
+ }
+
+ get preferredApplicationHandler() {
+ return this.wrappedHandlerInfo.preferredApplicationHandler;
+ }
+
+ set preferredApplicationHandler(aNewValue) {
+ this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue;
+
+ // Make sure the preferred handler is in the set of possible handlers.
+ if (aNewValue) {
+ this.addPossibleApplicationHandler(aNewValue);
+ }
+ }
+
+ get possibleApplicationHandlers() {
+ return this.wrappedHandlerInfo.possibleApplicationHandlers;
+ }
+
+ addPossibleApplicationHandler(aNewHandler) {
+ for (let app of this.possibleApplicationHandlers.enumerate()) {
+ if (app.equals(aNewHandler)) {
+ return;
+ }
+ }
+ this.possibleApplicationHandlers.appendElement(aNewHandler);
+ }
+
+ removePossibleApplicationHandler(aHandler) {
+ var defaultApp = this.preferredApplicationHandler;
+ if (defaultApp && aHandler.equals(defaultApp)) {
+ // If the app we remove was the default app, we must make sure
+ // it won't be used anymore
+ this.alwaysAskBeforeHandling = true;
+ this.preferredApplicationHandler = null;
+ }
+
+ var handlers = this.possibleApplicationHandlers;
+ for (var i = 0; i < handlers.length; ++i) {
+ var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp);
+ if (handler.equals(aHandler)) {
+ handlers.removeElementAt(i);
+ break;
+ }
+ }
+ }
+
+ get hasDefaultHandler() {
+ return this.wrappedHandlerInfo.hasDefaultHandler;
+ }
+
+ get defaultDescription() {
+ return this.wrappedHandlerInfo.defaultDescription;
+ }
+
+ // What to do with content of this type.
+ get preferredAction() {
+ // If the action is to use a helper app, but we don't have a preferred
+ // handler app, then switch to using the system default, if any; otherwise
+ // fall back to saving to disk, which is the default action in nsMIMEInfo.
+ // Note: "save to disk" is an invalid value for protocol info objects,
+ // but the alwaysAskBeforeHandling getter will detect that situation
+ // and always return true in that case to override this invalid value.
+ if (
+ this.wrappedHandlerInfo.preferredAction ==
+ Ci.nsIHandlerInfo.useHelperApp &&
+ !gMainPane.isValidHandlerApp(this.preferredApplicationHandler)
+ ) {
+ if (this.wrappedHandlerInfo.hasDefaultHandler) {
+ return Ci.nsIHandlerInfo.useSystemDefault;
+ }
+ return Ci.nsIHandlerInfo.saveToDisk;
+ }
+
+ return this.wrappedHandlerInfo.preferredAction;
+ }
+
+ set preferredAction(aNewValue) {
+ this.wrappedHandlerInfo.preferredAction = aNewValue;
+ }
+
+ get alwaysAskBeforeHandling() {
+ // If this is a protocol type and the preferred action is "save to disk",
+ // which is invalid for such types, then return true here to override that
+ // action. This could happen when the preferred action is to use a helper
+ // app, but the preferredApplicationHandler is invalid, and there isn't
+ // a default handler, so the preferredAction getter returns save to disk
+ // instead.
+ if (
+ !(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) &&
+ this.preferredAction == Ci.nsIHandlerInfo.saveToDisk
+ ) {
+ return true;
+ }
+
+ return this.wrappedHandlerInfo.alwaysAskBeforeHandling;
+ }
+
+ set alwaysAskBeforeHandling(aNewValue) {
+ this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue;
+ }
+
+ // The primary file extension associated with this type, if any.
+ get primaryExtension() {
+ try {
+ if (
+ this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ this.wrappedHandlerInfo.primaryExtension
+ ) {
+ return this.wrappedHandlerInfo.primaryExtension;
+ }
+ } catch (ex) {}
+
+ return null;
+ }
+
+ store() {
+ gHandlerService.store(this.wrappedHandlerInfo);
+ }
+
+ get smallIcon() {
+ return this._getIcon(16);
+ }
+
+ _getIcon(aSize) {
+ if (this.primaryExtension) {
+ return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize;
+ }
+
+ if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type;
+ }
+
+ // FIXME: consider returning some generic icon when we can't get a URL for
+ // one (for example in the case of protocol schemes). Filed as bug 395141.
+ return null;
+ }
+}
+
+/**
+ * InternalHandlerInfoWrapper provides a basic mechanism to create an internal
+ * mime type handler that can be enabled/disabled in the applications preference
+ * menu.
+ */
+class InternalHandlerInfoWrapper extends HandlerInfoWrapper {
+ constructor(mimeType, extension) {
+ let type = gMIMEService.getFromTypeAndExtension(mimeType, extension);
+ super(mimeType || type.type, type);
+ }
+
+ // Override store so we so we can notify any code listening for registration
+ // or unregistration of this handler.
+ store() {
+ super.store();
+ }
+
+ get preventInternalViewing() {
+ return false;
+ }
+
+ get enabled() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+}
+
+class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper {
+ constructor() {
+ super(TYPE_PDF, null);
+ }
+
+ get preventInternalViewing() {
+ return Services.prefs.getBoolPref(PREF_PDFJS_DISABLED);
+ }
+
+ // PDF is always shown in the list, but the 'show internally' option is
+ // hidden when the internal PDF viewer is disabled.
+ get enabled() {
+ return true;
+ }
+}
+
+class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper {
+ get enabled() {
+ return DownloadIntegration.shouldViewDownloadInternally(this.type);
+ }
+}
+
+const AppearanceChooser = {
+ // NOTE: This order must match the values of the
+ // layout.css.prefers-color-scheme.content-override
+ // preference.
+ choices: ["dark", "light", "auto"],
+ chooser: null,
+ radios: null,
+ warning: null,
+
+ init() {
+ this.chooser = document.getElementById("web-appearance-chooser");
+ this.radios = [...this.chooser.querySelectorAll("input")];
+ for (let radio of this.radios) {
+ radio.addEventListener("change", e => {
+ let index = this.choices.indexOf(e.target.value);
+ // The pref change callback will update state if needed.
+ if (index >= 0) {
+ Services.prefs.setIntPref(PREF_CONTENT_APPEARANCE, index);
+ } else {
+ // Shouldn't happen but let's do something sane...
+ Services.prefs.clearUserPref(PREF_CONTENT_APPEARANCE);
+ }
+ });
+ }
+
+ // Forward the click to the "colors" button.
+ document
+ .getElementById("web-appearance-manage-colors-link")
+ .addEventListener("click", function (e) {
+ document.getElementById("colors").click();
+ e.preventDefault();
+ });
+
+ document
+ .getElementById("web-appearance-manage-themes-link")
+ .addEventListener("click", function (e) {
+ window.browsingContext.topChromeWindow.BrowserOpenAddonsMgr(
+ "addons://list/theme"
+ );
+ e.preventDefault();
+ });
+
+ this.warning = document.getElementById("web-appearance-override-warning");
+
+ FORCED_COLORS_QUERY.addEventListener("change", this);
+ Services.prefs.addObserver(PREF_USE_SYSTEM_COLORS, this);
+ Services.obs.addObserver(this, "look-and-feel-changed");
+ this._update();
+ },
+
+ _update() {
+ this._updateWarning();
+ this._updateOptions();
+ },
+
+ handleEvent(e) {
+ this._update();
+ },
+
+ observe(subject, topic, data) {
+ this._update();
+ },
+
+ destroy() {
+ Services.prefs.removeObserver(PREF_USE_SYSTEM_COLORS, this);
+ Services.obs.removeObserver(this, "look-and-feel-changed");
+ FORCED_COLORS_QUERY.removeEventListener("change", this);
+ },
+
+ _isValueDark(value) {
+ switch (value) {
+ case "light":
+ return false;
+ case "dark":
+ return true;
+ case "auto":
+ return Services.appinfo.contentThemeDerivedColorSchemeIsDark;
+ }
+ throw new Error("Unknown value");
+ },
+
+ _updateOptions() {
+ let index = Services.prefs.getIntPref(PREF_CONTENT_APPEARANCE);
+ if (index < 0 || index >= this.choices.length) {
+ index = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref(PREF_CONTENT_APPEARANCE);
+ }
+ let value = this.choices[index];
+ for (let radio of this.radios) {
+ let checked = radio.value == value;
+ let isDark = this._isValueDark(radio.value);
+
+ radio.checked = checked;
+ radio.closest("label").classList.toggle("dark", isDark);
+ }
+ },
+
+ _updateWarning() {
+ let forcingColorsAndNoColorSchemeSupport =
+ FORCED_COLORS_QUERY.matches &&
+ (AppConstants.platform == "win" ||
+ !Services.prefs.getBoolPref(PREF_USE_SYSTEM_COLORS));
+ this.warning.hidden = !forcingColorsAndNoColorSchemeSupport;
+ },
+};
diff --git a/browser/components/preferences/more-from-mozilla-qr-code-simple-cn.svg b/browser/components/preferences/more-from-mozilla-qr-code-simple-cn.svg
new file mode 100644
index 0000000000..5052db9702
--- /dev/null
+++ b/browser/components/preferences/more-from-mozilla-qr-code-simple-cn.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="80" height="80" fill="context-fill" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 2h72a2 2 0 0 1 2 2v72a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2ZM0 4a4 4 0 0 1 4-4h72a4 4 0 0 1 4 4v72a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4Zm7.08 3.08h15.892v15.892H7.08V7.08Zm4.54 2.27H9.35V20.701H20.701V9.35h-9.08Zm18.163-2.27h4.541v2.27h2.27v2.27h-2.27v2.27h-2.27v2.271h4.54v-4.54h2.27V9.35h2.271v6.811h-2.27v2.27h2.27v4.541h-2.27v-2.27h-2.27v-2.27h-2.27v2.27h-2.271v-2.27h-2.27V7.08Zm0 13.622v-2.27h-2.27V9.35h-2.27v13.622h2.27v2.27h-9.081v2.271h-2.27v2.27H13.89v-4.54h-2.27v2.27H7.08v2.27h2.27v2.27h2.27v2.271h9.081v2.27h2.271v2.27h-2.27v2.271h-2.27v-2.27h-2.27v-2.27H7.08v2.27h4.54v2.27H9.35v2.27h6.811v-2.27h2.27v4.54h6.811v-2.27h2.271v2.27h2.27v-4.54h-4.54v-2.27h2.27v-4.54h2.27v-2.271h-4.54v-2.27h2.27v-2.271h2.27v-4.54h2.27v2.27h4.541v2.27h-2.27v2.27h2.27v-2.27h2.27v-4.54h-2.27v-2.271h-2.27v2.27h-2.27v-2.27h-2.27Zm0 0h-2.27v2.27h2.27v-2.27Zm-9.081 11.352v2.27h2.27v2.27h2.27v-4.54h-4.54Zm4.54 9.081h-4.54v2.27h4.54v-2.27Zm-4.54-9.081v-2.27h2.27v-2.271h-4.54v4.54h2.27Zm-9.082 0v-2.27h2.27v2.27h-2.27ZM41.135 9.35V7.08h2.27v2.27h-2.27Zm2.27 20.433h-2.27v-4.54h2.27v2.27h2.27v-2.27h2.271v-4.541h2.27v2.27h2.271v-4.54h2.27V25.242h2.27v2.271h-4.54v-2.27h-2.27v4.54h-6.811Zm9.082 2.27v-2.27h-2.27v2.27h2.27Zm2.27 0h-2.27v4.541h-2.27v4.541h2.27v4.54h-2.27v4.541h-4.541v2.271h-2.27v-2.27h-2.27v2.27h2.27v2.27h-4.541v-4.54h-6.811v2.27h-2.27v-4.54h-4.541v2.27h-2.27v-2.27h-2.271v2.27h2.27v2.27h-4.54v-4.54H13.89v2.27h-2.27v-4.541H9.35v2.27H7.08v2.27h4.54v2.271H7.08v2.27h4.54v-2.27h2.27v2.27h2.271v-2.27h2.27v2.27h6.811v6.811h2.271v2.27h2.27v2.271h-4.54v2.27h2.27v2.271h2.27v-4.54h2.27v-2.271h-2.27v-4.54h4.541v2.27h2.27v2.27h-2.27v2.27h2.27v4.541h-2.27v2.27h2.27v-2.27h2.27v2.27h6.811v-2.27h4.541v-4.54h4.541v2.27h-2.27v4.54h2.27v-2.27h2.27v2.27h9.082V66.11h2.27v2.27h4.541v-2.27h-2.27v-6.811h2.27V45.675h-2.27v13.622h-2.27v-2.27h-2.27v-4.541h-6.811v-2.27h-4.541v-6.811h2.27v4.54h4.54v-4.54h-2.27v-2.27h-6.811v-2.271h2.27v-2.27h2.27v2.27h2.271v-4.54h4.54v9.08h2.271v-2.27h2.27v-2.27h-2.27v-6.811h2.27v-2.27h2.271v2.27h2.27v-4.541h-2.27v-2.27H59.299v2.27h-2.271v2.27h-2.27v2.27Zm2.27 0v2.271h-2.27v-2.27h2.27Zm2.271-2.27h2.27v2.27h2.27v-4.54h-4.54v2.27Zm0 0v2.27h-2.27v-2.27h2.27Zm9.082 29.515h-2.27v2.27h-2.271v2.27h4.54v-4.54ZM54.757 66.11h2.27v2.27h2.271v2.271h4.54v-2.27h-2.27v-2.27h-2.27v-2.271h-4.54v2.27Zm-6.81-2.27v2.27h2.27v-2.27h-2.27Zm-2.271 0v-2.27h-6.811v-2.27h2.27v-2.271h2.27v-2.27h9.082v4.54h-4.54v-2.27h-2.271v2.27h2.27v4.54h-2.27Zm0 0h-9.081v2.27h2.27v2.27h2.27v2.271h2.27v-2.27h2.27v-4.541Zm-9.082-6.811v2.27h-2.27v-2.27h2.27Zm0-2.27h2.27v2.27h-2.27v-2.27Zm0 0h-2.27v-2.271h2.27v2.27Zm-6.81 4.54v-6.811h-2.271v6.811h2.27ZM47.945 7.08h4.541v4.54h-2.27V9.35h-2.27V7.08Zm0 2.27v2.27h2.27v2.27h4.541v2.271h-4.54v2.27h-6.811v-2.27h2.27v-2.27h-2.27v-2.27h2.27V9.35h2.27Zm9.082-2.27H72.92v15.892H57.028V7.08Zm4.54 2.27h-2.27V20.701H70.65V9.35h-9.081ZM11.62 11.62h6.811v6.811H11.621V11.621Zm49.949 0H68.379v6.811H61.57V11.621Zm-18.163 9.082h2.27v2.27h-2.27v-2.27ZM68.38 34.324h2.27v2.27h-2.27v-2.27Zm2.27 2.27h2.27v4.541h-2.27v-4.54Zm-4.54 11.352h2.27v2.27h-2.27v-2.27Zm-9.082 6.811h-2.27v6.811H61.567V54.758h-4.54ZM7.08 57.027h15.892V72.92H7.08V57.028Zm4.54 2.271H9.35V70.65H20.701V59.299h-9.08Zm45.408-2.27h2.27v2.27h-2.27v-2.27Zm-45.408 4.54h6.811v6.811H11.621V61.57Zm59.03 9.082h2.27v2.27h-2.27v-2.27Z"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99l.002.01c-.742-1.847-1.998-2.593-3.025-4.215-.052-.082-.104-.164-.154-.25a2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.024.024 0 0 0-.013 0l-.003.002-.005.003.003-.005c-1.647.964-2.206 2.749-2.256 3.642a3.28 3.28 0 0 0-1.805.696 1.953 1.953 0 0 0-.17-.129 3.04 3.04 0 0 1-.018-1.602 4.856 4.856 0 0 0-1.578 1.22h-.003c-.26-.33-.242-1.416-.227-1.643-.077.031-.15.07-.219.117-.229.163-.444.347-.64.549-.225.227-.43.473-.613.735a5.537 5.537 0 0 0-.88 1.986l-.009.043c-.012.057-.056.346-.064.41l-.002.014c-.057.298-.093.6-.106.903v.034a6.556 6.556 0 0 0 13.017 1.109l.03-.254a6.742 6.742 0 0 0-.426-3.293Zm-7.556 5.132c.03.015.059.03.09.044l.005.003a3.257 3.257 0 0 1-.095-.047Zm6.906-4.79v-.006.007Z" fill="url(#a)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99v.006l.002.007a5.937 5.937 0 0 1-.204 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093-.064.298-.099.6-.106.905v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.254a6.743 6.743 0 0 0-.426-3.293Z" fill="url(#b)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99v.006l.002.007a5.937 5.937 0 0 1-.204 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093-.064.298-.099.6-.106.905v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.254a6.743 6.743 0 0 0-.426-3.293Z" fill="url(#c)"/><path d="m42.879 38.322.04.03a3.57 3.57 0 0 0-.608-.795c-2.039-2.038-.535-4.418-.28-4.54l.002-.004c-1.647.965-2.206 2.75-2.257 3.642.077-.005.152-.011.23-.011a3.307 3.307 0 0 1 2.873 1.677Z" fill="url(#d)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.865 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.073.036.11.053.065.029.129.055.193.079.274.097.562.152.853.164 3.268.154 3.9-3.907 1.542-5.086a2.263 2.263 0 0 1 1.581.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#e)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.865 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.073.036.11.053.065.029.129.055.193.079.274.097.562.152.853.164 3.268.154 3.9-3.907 1.542-5.086a2.263 2.263 0 0 1 1.581.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#f)"/><path d="m37.665 37.134.136.09a3.04 3.04 0 0 1-.019-1.602 4.854 4.854 0 0 0-1.578 1.22c.032-.002.983-.019 1.461.292Z" fill="url(#g)"/><path d="M33.503 40.145c.506 2.989 3.216 5.272 6.291 5.359 2.847.08 4.665-1.572 5.416-3.184a5.937 5.937 0 0 0 .204-4.425v-.012l.002.01c.232 1.52-.54 2.99-1.748 3.986l-.004.008c-2.352 1.917-4.604 1.156-5.059.846a3.524 3.524 0 0 1-.095-.047c-1.372-.656-1.939-1.905-1.817-2.977a1.683 1.683 0 0 1-1.553-.977 2.474 2.474 0 0 1 2.41-.097c.777.352 1.66.387 2.462.097-.003-.054-1.144-.508-1.589-.946-.238-.234-.35-.347-.45-.432a1.987 1.987 0 0 0-.17-.128 9.602 9.602 0 0 0-.135-.09c-.478-.31-1.429-.294-1.46-.293h-.003c-.26-.33-.242-1.415-.227-1.642-.077.03-.15.07-.219.116-.23.164-.444.348-.64.55a5.712 5.712 0 0 0-.616.733 5.536 5.536 0 0 0-.88 1.986c-.003.013-.236 1.031-.121 1.56l.001-.001Z" fill="url(#h)"/><path d="M42.31 37.557c.24.235.445.503.61.795.035.027.07.054.098.08 1.486 1.37.707 3.307.65 3.444 1.207-.994 1.978-2.466 1.747-3.985-.742-1.849-2-2.595-3.025-4.217a7.809 7.809 0 0 1-.154-.25 2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.024.024 0 0 0-.013 0l-.003.002-.004.003c-.254.12-1.758 2.501.28 4.538v.003Z" fill="url(#i)"/><path d="M43.017 38.433a1.349 1.349 0 0 0-.098-.08l-.04-.03a2.263 2.263 0 0 0-1.581-.384c2.358 1.179 1.725 5.239-1.543 5.086a2.915 2.915 0 0 1-.853-.164 3.434 3.434 0 0 1-.303-.132l.005.003c.455.311 2.706 1.071 5.059-.846l.004-.008c.058-.137.837-2.074-.65-3.444Z" fill="url(#j)"/><path d="M37.055 40.581s.302-1.127 2.167-1.127c.201 0 .778-.563.788-.726a3.265 3.265 0 0 1-2.461-.097 2.471 2.471 0 0 0-2.41.097 1.683 1.683 0 0 0 1.552.977c-.121 1.072.446 2.32 1.817 2.977l.091.045c-.8-.414-1.462-1.196-1.544-2.145Z" fill="url(#k)"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.316-1.66.323.625.547 1.296.664 1.99l.002.01c-.742-1.847-1.998-2.593-3.025-4.215a8.036 8.036 0 0 1-.154-.25 2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.024.024 0 0 0-.013 0l-.003.002-.005.003.003-.005c-1.647.964-2.206 2.749-2.256 3.642.076-.005.152-.011.23-.011a3.308 3.308 0 0 1 2.872 1.677 2.263 2.263 0 0 0-1.58-.384c2.357 1.18 1.725 5.239-1.543 5.087a2.916 2.916 0 0 1-.853-.165 3.432 3.432 0 0 1-.303-.132l.004.003a3.524 3.524 0 0 1-.095-.047l.09.044c-.8-.413-1.461-1.195-1.544-2.144 0 0 .303-1.128 2.167-1.128.202 0 .778-.563.789-.726-.003-.053-1.144-.507-1.589-.945-.237-.234-.35-.347-.45-.432a1.987 1.987 0 0 0-.17-.128 3.04 3.04 0 0 1-.018-1.603 4.854 4.854 0 0 0-1.578 1.22h-.003c-.26-.33-.241-1.415-.226-1.642-.077.03-.15.07-.22.116-.229.164-.443.347-.64.549-.224.227-.43.473-.613.735v.001-.001a5.536 5.536 0 0 0-.88 1.986l-.008.043c-.013.058-.068.35-.076.414-.05.302-.083.607-.097.914v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.253a6.743 6.743 0 0 0-.426-3.294Zm-.65.337v.006-.007Z" fill="url(#l)"/><defs><radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(44.777 34.536) scale(13.6677)"><stop offset=".129" stop-color="#FFBD4F"/><stop offset=".186" stop-color="#FFAC31"/><stop offset=".247" stop-color="#FF9D17"/><stop offset=".283" stop-color="#FF980E"/><stop offset=".403" stop-color="#FF563B"/><stop offset=".467" stop-color="#FF3750"/><stop offset=".71" stop-color="#F5156C"/><stop offset=".782" stop-color="#EB0878"/><stop offset=".86" stop-color="#E50080"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.702 40.118) scale(13.6677)"><stop offset=".3" stop-color="#960E18"/><stop offset=".351" stop-color="#B11927" stop-opacity=".74"/><stop offset=".435" stop-color="#DB293D" stop-opacity=".343"/><stop offset=".497" stop-color="#F5334B" stop-opacity=".094"/><stop offset=".53" stop-color="#FF3750" stop-opacity="0"/></radialGradient><radialGradient id="d" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(41.394 31.491) scale(9.90066)"><stop offset=".132" stop-color="#FFF44F"/><stop offset=".252" stop-color="#FFDC3E"/><stop offset=".506" stop-color="#FF9D12"/><stop offset=".526" stop-color="#FF980E"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(38.18 43.67) scale(6.5074)"><stop offset=".353" stop-color="#3A8EE6"/><stop offset=".472" stop-color="#5C79F0"/><stop offset=".669" stop-color="#9059FF"/><stop offset="1" stop-color="#C139E6"/></radialGradient><radialGradient id="f" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.35414 -.81093 .9494 3.92687 40.363 38.945)"><stop offset=".206" stop-color="#9059FF" stop-opacity="0"/><stop offset=".278" stop-color="#8C4FF3" stop-opacity=".064"/><stop offset=".747" stop-color="#7716A8" stop-opacity=".45"/><stop offset=".975" stop-color="#6E008B" stop-opacity=".6"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 34.028) scale(4.68222)"><stop stop-color="#FFE226"/><stop offset=".121" stop-color="#FFDB27"/><stop offset=".295" stop-color="#FFC82A"/><stop offset=".502" stop-color="#FFA930"/><stop offset=".732" stop-color="#FF7E37"/><stop offset=".792" stop-color="#FF7139"/></radialGradient><radialGradient id="h" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.254 30.983) scale(19.9772)"><stop offset=".113" stop-color="#FFF44F"/><stop offset=".456" stop-color="#FF980E"/><stop offset=".622" stop-color="#FF5634"/><stop offset=".716" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="i" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(83.976 2.969 39.153) scale(14.6396 9.60783)"><stop stop-color="#FFF44F"/><stop offset=".06" stop-color="#FFE847"/><stop offset=".168" stop-color="#FFC830"/><stop offset=".304" stop-color="#FF980E"/><stop offset=".356" stop-color="#FF8B16"/><stop offset=".455" stop-color="#FF672A"/><stop offset=".57" stop-color="#FF3647"/><stop offset=".737" stop-color="#E31587"/></radialGradient><radialGradient id="j" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 35.72) scale(12.4701)"><stop offset=".137" stop-color="#FFF44F"/><stop offset=".48" stop-color="#FF980E"/><stop offset=".592" stop-color="#FF5634"/><stop offset=".655" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="k" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(42.747 36.397) scale(13.6491)"><stop offset=".094" stop-color="#FFF44F"/><stop offset=".231" stop-color="#FFE141"/><stop offset=".509" stop-color="#FFAF1E"/><stop offset=".626" stop-color="#FF980E"/></radialGradient><linearGradient id="a" x1="45.198" y1="35.108" x2="34.314" y2="45.609" gradientUnits="userSpaceOnUse"><stop offset=".048" stop-color="#FFF44F"/><stop offset=".111" stop-color="#FFE847"/><stop offset=".225" stop-color="#FFC830"/><stop offset=".368" stop-color="#FF980E"/><stop offset=".401" stop-color="#FF8B16"/><stop offset=".462" stop-color="#FF672A"/><stop offset=".534" stop-color="#FF3647"/><stop offset=".705" stop-color="#E31587"/></linearGradient><linearGradient id="l" x1="45.066" y1="35.053" x2="35.806" y2="44.314" gradientUnits="userSpaceOnUse"><stop offset=".167" stop-color="#FFF44F" stop-opacity=".8"/><stop offset=".266" stop-color="#FFF44F" stop-opacity=".634"/><stop offset=".489" stop-color="#FFF44F" stop-opacity=".217"/><stop offset=".6" stop-color="#FFF44F" stop-opacity="0"/></linearGradient></defs></svg>
diff --git a/browser/components/preferences/more-from-mozilla-qr-code-simple.svg b/browser/components/preferences/more-from-mozilla-qr-code-simple.svg
new file mode 100644
index 0000000000..279595db10
--- /dev/null
+++ b/browser/components/preferences/more-from-mozilla-qr-code-simple.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="80" height="80" fill="context-fill" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M76 2H4a2 2 0 0 0-2 2v72a2 2 0 0 0 2 2h72a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2ZM4 0a4 4 0 0 0-4 4v72a4 4 0 0 0 4 4h72a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4H4Zm3.08 7.08h15.892v15.892H7.08V7.08Zm4.54 2.27H9.35V20.701H20.701V9.35h-9.08Zm20.434-2.27h2.27v2.27h2.27V7.08h4.541v2.27h2.27v2.27H36.596v2.27h-4.541v2.271h-2.27v-2.27h-2.271v-2.27h2.27V9.35h2.27V7.08Zm-4.541 4.54h-2.27V9.35h2.27v2.27Zm2.27 11.352v-4.54h2.27v-2.27h4.541V13.89h4.541v2.27h4.54v2.27h-2.27v2.27h-2.27v-2.27h-2.27v2.27h-2.27v-2.27h-2.27v2.27h-2.271v2.271h-2.27Zm-4.54 2.27V18.433h2.27v4.54h2.27v2.27h-4.54Zm0 0v2.271h-9.081v-2.27h9.08Zm11.351-2.27h-2.27v-2.27h2.27v2.27Zm2.27 0h-2.27v4.541h-2.27v-2.27h-2.27v2.27h2.27v2.27h4.54V22.973Zm2.271 0v-2.27h-2.27v2.27h2.27Zm2.27 0h-2.27v6.811h4.54v-2.27h-2.27v-4.54Zm2.27 0h-2.27v-2.27h2.27v2.27Zm4.542-6.81h-2.27V22.971h-2.271v2.27h2.27v2.271h2.27v2.27h-2.27v2.27h2.27v-2.27h2.271v2.27h2.27v2.271h2.27v-4.54h2.271v2.27h2.27v2.27h-2.27v2.27h-2.27v2.27h-4.541v6.811h2.27v2.271h4.541v4.541h-6.811v-2.27h-2.27v2.27h-2.27v-2.27h-4.541v2.27h-4.541v-2.27h-4.54v2.27h2.27v2.27h2.27v2.27H45.675v4.541h2.271v2.27h-2.27v2.271h-2.27v-4.54h-4.541v-2.27h-2.27v-2.271h-2.27v2.27h2.27v2.27h-2.27v2.27h-2.271v-2.27h-2.27v-2.27h-2.271v-4.54h2.27v2.27h2.27v-2.27h-2.27v-2.271h2.27v-2.27h-4.54v-2.27h2.27v-2.271h-2.27v-4.54h2.27v-2.271h-2.27v-6.811h2.27v-2.27h-2.27v2.27h-4.54v-2.27h-2.271v2.27h2.27v2.27h-4.54v-4.54h-2.27V36.593H13.89v-4.54h-2.27v-2.27h2.27v-2.271H7.08v6.811h2.27v4.54h4.54v2.271h-2.27v2.27H9.35v-2.27H7.08v2.27h2.27v2.27H7.08v2.271h2.27v-2.27h2.27v2.27h2.27v-4.54h4.541v-2.27h-2.27v-4.541h2.27v2.27h2.27v2.27h2.271v2.27h-2.27v2.27h-2.27v2.271h-2.27v2.27H11.62v2.271H7.08v2.27h4.54v-2.27h4.541v2.27h9.081v9.081h4.541v4.541h-2.27v4.541h4.54v-2.27h2.271v2.27h2.27v-4.54h-4.54v-2.27h2.27v-2.271h2.27v-2.27h2.27v2.27h2.271v2.27h-2.27v6.811H45.675v-4.54h2.271v4.54h2.27v-4.54h2.271v4.54h6.811v-2.27h2.27v2.27h2.27v-2.27h-2.27v-2.27h-4.54v-2.27h-9.081v-2.271H59.297v2.27h2.27v-2.27h2.27v4.54h4.541v4.541h2.271v-2.27h2.27v-4.54h-2.27v-2.271h2.27v-2.27h-2.27v-2.27h2.27v-4.541h-2.27v-4.541h2.27V27.513h-2.27v-2.27h-2.27v2.27h-6.811v-2.27h-4.541v2.27h-2.27v-2.27h-2.271v-2.27h2.27v-4.541h-4.54v-2.27Zm2.27-6.812h2.27v4.54h-2.27v2.271h-2.27v-4.54h2.27V9.35Zm0 0h-6.811V7.08h6.811v2.27Zm0 13.622v-2.27h-2.27v2.27h2.27Zm9.081 6.811v2.27h2.27v2.271h-2.27v2.27h-2.27v4.541h-4.54v2.27h2.27v2.27h4.54v-2.27h2.27v4.541h-2.27v4.541h2.27v6.811h2.271v2.27h-2.27v2.27h4.54v-4.54h2.271v-4.54h-2.27v-4.541h2.27v-4.541h-2.27v-4.54h2.27v-6.811h-2.27v-2.271h2.27v-2.27h-4.54v2.27h-2.271v-2.27h-2.27Zm6.812 24.974h-2.27v2.27h2.27v-2.27Zm0-4.54h-4.541v-2.27h4.54v2.27Zm-4.541-9.082h2.27v2.27h-2.27v-2.27Zm-2.27 0v-4.54h2.27v4.54h-2.27Zm4.54-4.54v-2.27h-2.27v2.27h2.27Zm0 0h2.27v2.27h-2.27v-2.27Zm-4.54 4.54v2.27h-2.27v-2.27h2.27Zm0-11.352v-2.27h-2.27v2.27h2.27ZM57.028 68.38h-2.27v2.27h2.27v-2.27Zm-24.974 0h-2.27v2.27h2.27v-2.27ZM20.702 47.946v2.27h2.27v2.271h-4.54v-4.54h2.27Zm2.27-2.27v2.27h-2.27v-2.27h2.27Zm2.27 0v2.27h2.271v-2.27h-2.27Zm0 0h-2.27v-2.27h2.27v2.27Zm-2.27-9.082v2.27h-2.27v-2.27h2.27Zm0 0h2.27v-2.27h-2.27v2.27ZM9.35 34.324v-2.27h2.27v2.27H9.35Zm15.893 20.433h2.27v-2.27h-2.27v2.27ZM41.135 66.11v4.541h2.27v-4.54h-2.27Zm6.811-4.54v-2.27h2.27v2.27h-2.27Zm-2.27-6.812h-2.27v-2.27h2.27v2.27Zm0 0v2.27h2.27v-2.27h-2.27Zm4.54-20.433v2.27h2.271v-2.27h-2.27Zm2.271-9.081h-2.27v2.27h2.27v-2.27Zm4.54-18.163H72.92v15.892H57.028V7.08Zm4.541 2.27h-2.27V20.701H70.65V9.35h-9.081ZM11.62 11.62h6.811v6.811H11.621V11.621Zm34.056 0h2.27v2.27h-2.27v-2.27Zm15.892 0h6.811v6.811H61.57V11.621Zm-4.54 43.137h-2.27v6.811H61.567V54.758h-4.54ZM7.08 57.027h15.892V72.92H7.08V57.028Zm4.54 2.271H9.35V70.65H20.701V59.299h-9.08Zm45.408-2.27h2.27v2.27h-2.27v-2.27Zm-45.408 4.54h6.811v6.811H11.621V61.57Z"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.315-1.66a6.81 6.81 0 0 1 .663 1.99l.002.01c-.741-1.847-1.998-2.593-3.025-4.215a8.23 8.23 0 0 1-.154-.25 2.078 2.078 0 0 1-.072-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.011-.006.024.024 0 0 0-.013 0l-.003.002-.004.003.002-.005c-1.647.964-2.206 2.749-2.256 3.642a3.28 3.28 0 0 0-1.805.696 1.967 1.967 0 0 0-.17-.129 3.04 3.04 0 0 1-.018-1.602 4.855 4.855 0 0 0-1.578 1.22h-.003c-.26-.33-.242-1.416-.227-1.643-.076.031-.15.07-.218.117-.23.163-.444.347-.641.549a5.73 5.73 0 0 0-.613.735 5.535 5.535 0 0 0-.88 1.986l-.008.043c-.013.057-.057.346-.065.41l-.002.014c-.057.298-.092.6-.106.903v.034a6.556 6.556 0 0 0 13.017 1.109l.03-.254a6.743 6.743 0 0 0-.426-3.293Zm-7.556 5.132c.03.015.059.03.09.044l.005.003a3.257 3.257 0 0 1-.095-.047Zm6.906-4.79v-.006l.001.007h-.001Z" fill="url(#a)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.315-1.66a6.81 6.81 0 0 1 .664 1.99V37.895a5.937 5.937 0 0 1-.203 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093a4.88 4.88 0 0 0-.105.905v.034a6.557 6.557 0 0 0 13.016 1.108c.011-.084.02-.168.03-.254a6.742 6.742 0 0 0-.425-3.293h-.001Z" fill="url(#b)"/><path d="M46.066 37.552c-.285-.685-.863-1.426-1.315-1.66a6.81 6.81 0 0 1 .664 1.99V37.895a5.937 5.937 0 0 1-.203 4.425c-.752 1.612-2.57 3.265-5.417 3.184-3.075-.088-5.785-2.37-6.29-5.36-.093-.47 0-.71.046-1.093a4.88 4.88 0 0 0-.105.905v.034a6.557 6.557 0 0 0 13.016 1.108c.011-.084.02-.168.03-.254a6.742 6.742 0 0 0-.425-3.293h-.001Z" fill="url(#c)"/><path d="m42.879 38.322.04.03a3.567 3.567 0 0 0-.608-.795c-2.038-2.038-.534-4.418-.28-4.54l.002-.004c-1.647.965-2.206 2.75-2.257 3.642.077-.005.153-.011.23-.011a3.308 3.308 0 0 1 2.873 1.677Z" fill="url(#d)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.864 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.074.036.11.053.065.029.129.055.193.079.275.097.562.152.853.164 3.268.154 3.9-3.907 1.543-5.086a2.263 2.263 0 0 1 1.58.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#e)"/><path d="M40.01 38.73c-.01.162-.587.725-.788.725-1.864 0-2.167 1.127-2.167 1.127.082.95.744 1.733 1.544 2.145.036.02.074.036.11.053.065.029.129.055.193.079.275.097.562.152.853.164 3.268.154 3.9-3.907 1.543-5.086a2.263 2.263 0 0 1 1.58.384 3.309 3.309 0 0 0-2.872-1.678c-.078 0-.154.007-.23.012a3.28 3.28 0 0 0-1.805.695c.1.085.213.198.45.432.445.439 1.586.893 1.589.946v.001Z" fill="url(#f)"/><path d="M37.666 37.134c.053.034.096.063.135.09a3.04 3.04 0 0 1-.019-1.602 4.854 4.854 0 0 0-1.578 1.22c.032-.002.983-.019 1.462.292Z" fill="url(#g)"/><path d="M33.503 40.145c.506 2.989 3.216 5.272 6.291 5.359 2.847.08 4.665-1.572 5.416-3.184a5.937 5.937 0 0 0 .204-4.425v-.012l.002.01c.232 1.52-.54 2.99-1.748 3.986l-.004.008c-2.352 1.917-4.604 1.156-5.059.846a3.524 3.524 0 0 1-.095-.047c-1.372-.656-1.939-1.905-1.817-2.977a1.684 1.684 0 0 1-1.553-.977 2.474 2.474 0 0 1 2.41-.097c.777.352 1.66.387 2.462.097-.003-.054-1.144-.508-1.589-.946-.237-.234-.35-.347-.45-.432a1.973 1.973 0 0 0-.17-.128 9.602 9.602 0 0 0-.135-.09c-.478-.31-1.429-.294-1.46-.293h-.003c-.26-.33-.242-1.415-.227-1.642-.077.03-.15.07-.219.116-.23.164-.444.348-.64.55a5.72 5.72 0 0 0-.616.733 5.538 5.538 0 0 0-.88 1.986c-.003.013-.236 1.031-.121 1.56l.001-.001Z" fill="url(#h)"/><path d="M42.31 37.557c.24.235.445.503.61.795.035.027.07.054.098.08 1.486 1.37.707 3.307.65 3.444 1.207-.994 1.978-2.466 1.747-3.985-.742-1.849-2-2.595-3.025-4.217a7.809 7.809 0 0 1-.154-.25 2.078 2.078 0 0 1-.072-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.011-.006.023.023 0 0 0-.013 0l-.003.002-.004.003c-.254.12-1.758 2.501.28 4.538v.003Z" fill="url(#i)"/><path d="M43.018 38.433a1.351 1.351 0 0 0-.099-.08l-.04-.03a2.263 2.263 0 0 0-1.581-.384c2.358 1.179 1.725 5.239-1.543 5.086a2.916 2.916 0 0 1-.853-.164 3.497 3.497 0 0 1-.192-.08c-.037-.016-.074-.033-.11-.052l.004.003c.455.311 2.706 1.071 5.06-.846l.003-.008c.058-.137.837-2.074-.65-3.444Z" fill="url(#j)"/><path d="M37.055 40.581s.302-1.127 2.167-1.127c.201 0 .778-.563.788-.726a3.265 3.265 0 0 1-2.461-.097 2.471 2.471 0 0 0-2.41.097 1.684 1.684 0 0 0 1.552.977c-.121 1.072.446 2.32 1.817 2.977l.091.045c-.8-.414-1.462-1.196-1.544-2.145Z" fill="url(#k)"/><path d="M46.066 37.552c-.285-.686-.863-1.426-1.316-1.66a6.81 6.81 0 0 1 .664 1.99l.002.01c-.742-1.847-1.998-2.593-3.025-4.215a8.23 8.23 0 0 1-.154-.25 2.078 2.078 0 0 1-.073-.136 1.193 1.193 0 0 1-.102-.271.017.017 0 0 0-.01-.006.023.023 0 0 0-.013 0l-.003.002-.005.003.003-.005c-1.647.964-2.206 2.749-2.256 3.642.076-.005.152-.011.23-.011a3.307 3.307 0 0 1 2.872 1.677 2.263 2.263 0 0 0-1.58-.384c2.357 1.18 1.725 5.239-1.543 5.087a2.916 2.916 0 0 1-.853-.165 3.495 3.495 0 0 1-.193-.079c-.037-.017-.074-.033-.11-.053l.004.003a3.524 3.524 0 0 1-.095-.047l.09.044c-.8-.413-1.461-1.195-1.544-2.144 0 0 .303-1.128 2.167-1.128.202 0 .779-.563.789-.726-.003-.053-1.144-.507-1.589-.945-.237-.234-.35-.347-.45-.432a1.988 1.988 0 0 0-.17-.128 3.04 3.04 0 0 1-.018-1.603 4.854 4.854 0 0 0-1.578 1.22h-.003c-.26-.33-.241-1.415-.226-1.642-.077.03-.15.07-.22.116-.229.164-.443.347-.64.549-.224.227-.43.473-.613.735v.001-.001a5.538 5.538 0 0 0-.88 1.986l-.008.043c-.013.058-.068.35-.076.414-.05.302-.083.607-.097.914v.034a6.557 6.557 0 0 0 13.017 1.108l.03-.253a6.742 6.742 0 0 0-.426-3.294Zm-.65.337v.006-.007Z" fill="url(#l)"/><defs><radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(44.777 34.536) scale(13.6677)"><stop offset=".129" stop-color="#FFBD4F"/><stop offset=".186" stop-color="#FFAC31"/><stop offset=".247" stop-color="#FF9D17"/><stop offset=".283" stop-color="#FF980E"/><stop offset=".403" stop-color="#FF563B"/><stop offset=".467" stop-color="#FF3750"/><stop offset=".71" stop-color="#F5156C"/><stop offset=".782" stop-color="#EB0878"/><stop offset=".86" stop-color="#E50080"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.702 40.118) scale(13.6677)"><stop offset=".3" stop-color="#960E18"/><stop offset=".351" stop-color="#B11927" stop-opacity=".74"/><stop offset=".435" stop-color="#DB293D" stop-opacity=".343"/><stop offset=".497" stop-color="#F5334B" stop-opacity=".094"/><stop offset=".53" stop-color="#FF3750" stop-opacity="0"/></radialGradient><radialGradient id="d" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(41.394 31.491) scale(9.90066)"><stop offset=".132" stop-color="#FFF44F"/><stop offset=".252" stop-color="#FFDC3E"/><stop offset=".506" stop-color="#FF9D12"/><stop offset=".526" stop-color="#FF980E"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(38.18 43.67) scale(6.50741)"><stop offset=".353" stop-color="#3A8EE6"/><stop offset=".472" stop-color="#5C79F0"/><stop offset=".669" stop-color="#9059FF"/><stop offset="1" stop-color="#C139E6"/></radialGradient><radialGradient id="f" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.35414 -.81093 .9494 3.92687 40.363 38.945)"><stop offset=".206" stop-color="#9059FF" stop-opacity="0"/><stop offset=".278" stop-color="#8C4FF3" stop-opacity=".064"/><stop offset=".747" stop-color="#7716A8" stop-opacity=".45"/><stop offset=".975" stop-color="#6E008B" stop-opacity=".6"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 34.028) scale(4.68221)"><stop stop-color="#FFE226"/><stop offset=".121" stop-color="#FFDB27"/><stop offset=".295" stop-color="#FFC82A"/><stop offset=".502" stop-color="#FFA930"/><stop offset=".732" stop-color="#FF7E37"/><stop offset=".792" stop-color="#FF7139"/></radialGradient><radialGradient id="h" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.254 30.983) scale(19.9772)"><stop offset=".113" stop-color="#FFF44F"/><stop offset=".456" stop-color="#FF980E"/><stop offset=".622" stop-color="#FF5634"/><stop offset=".716" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="i" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.53635 14.55876 -9.55479 1.0083 41.594 32.091)"><stop stop-color="#FFF44F"/><stop offset=".06" stop-color="#FFE847"/><stop offset=".168" stop-color="#FFC830"/><stop offset=".304" stop-color="#FF980E"/><stop offset=".356" stop-color="#FF8B16"/><stop offset=".455" stop-color="#FF672A"/><stop offset=".57" stop-color="#FF3647"/><stop offset=".737" stop-color="#E31587"/></radialGradient><radialGradient id="j" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.533 35.72) scale(12.4701)"><stop offset=".137" stop-color="#FFF44F"/><stop offset=".48" stop-color="#FF980E"/><stop offset=".592" stop-color="#FF5634"/><stop offset=".655" stop-color="#FF3647"/><stop offset=".904" stop-color="#E31587"/></radialGradient><radialGradient id="k" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(42.747 36.397) scale(13.6491)"><stop offset=".094" stop-color="#FFF44F"/><stop offset=".231" stop-color="#FFE141"/><stop offset=".509" stop-color="#FFAF1E"/><stop offset=".626" stop-color="#FF980E"/></radialGradient><linearGradient id="a" x1="45.198" y1="35.108" x2="34.314" y2="45.609" gradientUnits="userSpaceOnUse"><stop offset=".048" stop-color="#FFF44F"/><stop offset=".111" stop-color="#FFE847"/><stop offset=".225" stop-color="#FFC830"/><stop offset=".368" stop-color="#FF980E"/><stop offset=".401" stop-color="#FF8B16"/><stop offset=".462" stop-color="#FF672A"/><stop offset=".534" stop-color="#FF3647"/><stop offset=".705" stop-color="#E31587"/></linearGradient><linearGradient id="l" x1="45.066" y1="35.053" x2="35.806" y2="44.314" gradientUnits="userSpaceOnUse"><stop offset=".167" stop-color="#FFF44F" stop-opacity=".8"/><stop offset=".266" stop-color="#FFF44F" stop-opacity=".634"/><stop offset=".489" stop-color="#FFF44F" stop-opacity=".217"/><stop offset=".6" stop-color="#FFF44F" stop-opacity="0"/></linearGradient></defs></svg> \ No newline at end of file
diff --git a/browser/components/preferences/moreFromMozilla.inc.xhtml b/browser/components/preferences/moreFromMozilla.inc.xhtml
new file mode 100644
index 0000000000..6db36392e0
--- /dev/null
+++ b/browser/components/preferences/moreFromMozilla.inc.xhtml
@@ -0,0 +1,44 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!-- More From Mozilla panel -->
+
+<script src="chrome://browser/content/preferences/moreFromMozilla.js"/>
+<html:template xmlns="http://www.w3.org/1999/xhtml" id="template-paneMoreFromMozilla">
+ <div id="moreFromMozillaCategory-header"
+ class="subcategory"
+ data-hidden-from-search="true"
+ hidden="true"
+ data-category="paneMoreFromMozilla">
+ <h1 class="title" data-l10n-id="more-from-moz-title"/>
+ <p class="subtitle" data-l10n-id="more-from-moz-subtitle"/>
+ </div>
+ <div id="moreFromMozillaCategory"
+ data-category="paneMoreFromMozilla"
+ hidden="true"
+ data-hidden-from-search="true">
+ </div>
+</html:template>
+
+<html:template xmlns="http://www.w3.org/1999/xhtml" id="simple">
+ <article class="mozilla-product-item simple">
+
+ <div>
+ <h2 class="product-title"/>
+ <div class="product-description-box">
+ <div class="description"/>
+ <a class="text-link" target="_blank" hidden="true"/>
+ </div>
+ </div>
+
+ <button type="button" class="primary small-button" hidden="true"/>
+
+ <div class="qr-code-box" hidden="true">
+ <h3 class="qr-code-box-title"/>
+ <img class="qr-code-box-image"/>
+ <a class="qr-code-link text-link" target="_blank" hidden="true"/>
+ </div>
+
+ </article>
+</html:template>
diff --git a/browser/components/preferences/moreFromMozilla.js b/browser/components/preferences/moreFromMozilla.js
new file mode 100644
index 0000000000..f9cd0c8a82
--- /dev/null
+++ b/browser/components/preferences/moreFromMozilla.js
@@ -0,0 +1,271 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from preferences.js */
+
+var gMoreFromMozillaPane = {
+ initialized: false,
+
+ /**
+ * "default" is whatever template is the default, as defined by the code
+ * in this file (currently in `getTemplateName`). Setting option to an
+ * invalid value will leave it unchanged.
+ */
+ _option: "default",
+ set option(value) {
+ if (!value) {
+ this._option = "default";
+ return;
+ }
+
+ if (value === "default" || value === "simple") {
+ this._option = value;
+ }
+ },
+
+ get option() {
+ return this._option;
+ },
+
+ getTemplateName() {
+ if (!this._option || this._option == "default") {
+ return "simple";
+ }
+ return this._option;
+ },
+
+ getURL(url, region, option, hasEmail) {
+ const URL_PARAMS = {
+ utm_source: "about-prefs",
+ utm_campaign: "morefrommozilla",
+ utm_medium: "firefox-desktop",
+ };
+ // UTM content param used in analytics to record
+ // UI template used to open URL
+ const utm_content = {
+ default: "default",
+ simple: "fxvt-113-a",
+ };
+
+ const experiment_params = {
+ entrypoint_experiment: "morefrommozilla-experiment-1846",
+ };
+
+ let pageUrl = new URL(url);
+ for (let [key, val] of Object.entries(URL_PARAMS)) {
+ pageUrl.searchParams.append(key, val);
+ }
+
+ // Append region by product to utm_content and also
+ // append '-email' when URL is opened
+ // from send email link in QRCode box
+ if (option) {
+ pageUrl.searchParams.set(
+ "utm_content",
+ `${utm_content[option]}-${region}${hasEmail ? "-email" : ""}`
+ );
+ }
+
+ // Add experiments params when user is shown an experimental UI
+ // with template value as 'simple' set via Nimbus
+ if (option !== "default") {
+ pageUrl.searchParams.set(
+ "entrypoint_experiment",
+ experiment_params.entrypoint_experiment
+ );
+ pageUrl.searchParams.set("entrypoint_variation", `treatment-${option}`);
+ }
+ return pageUrl.toString();
+ },
+
+ renderProducts() {
+ let products = [
+ {
+ id: "firefox-mobile",
+ title_string_id: "more-from-moz-firefox-mobile-title",
+ description_string_id: "more-from-moz-firefox-mobile-description",
+ region: "global",
+ button: {
+ id: "fxMobile",
+ type: "link",
+ label_string_id: "more-from-moz-learn-more-link",
+ actionURL: AppConstants.isChinaRepack()
+ ? "https://www.firefox.com.cn/browsers/mobile/"
+ : "https://www.mozilla.org/firefox/browsers/mobile/",
+ },
+ qrcode: {
+ title: {
+ string_id: "more-from-moz-qr-code-box-firefox-mobile-title",
+ },
+ image_src_prefix:
+ "chrome://browser/content/preferences/more-from-mozilla-qr-code",
+ button: {
+ id: "qr-code-send-email",
+ label: {
+ string_id: "more-from-moz-qr-code-box-firefox-mobile-button",
+ },
+ actionURL: AppConstants.isChinaRepack()
+ ? "https://www.firefox.com.cn/mobile/get-app/"
+ : "https://www.mozilla.org/firefox/mobile/get-app/?v=mfm",
+ },
+ },
+ },
+ ];
+
+ if (BrowserUtils.shouldShowVPNPromo()) {
+ const vpn = {
+ id: "mozilla-vpn",
+ title_string_id: "more-from-moz-mozilla-vpn-title",
+ description_string_id: "more-from-moz-mozilla-vpn-description",
+ region: "global",
+ button: {
+ id: "mozillaVPN",
+ label_string_id: "more-from-moz-button-mozilla-vpn-2",
+ actionURL: "https://www.mozilla.org/products/vpn/",
+ },
+ };
+ products.push(vpn);
+ }
+
+ if (BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY)) {
+ const relay = {
+ id: "firefox-relay",
+ title_string_id: "more-from-moz-firefox-relay-title",
+ description_string_id: "more-from-moz-firefox-relay-description",
+ region: "global",
+ button: {
+ id: "firefoxRelay",
+ label_string_id: "more-from-moz-firefox-relay-button",
+ actionURL: "https://relay.firefox.com/",
+ },
+ };
+ products.push(relay);
+ }
+
+ this._productsContainer = document.getElementById(
+ "moreFromMozillaCategory"
+ );
+ let frag = document.createDocumentFragment();
+ this._template = document.getElementById(this.getTemplateName());
+
+ // Exit when internal data is incomplete
+ if (!this._template) {
+ return;
+ }
+
+ for (let product of products) {
+ let template = this._template.content.cloneNode(true);
+ let title = template.querySelector(".product-title");
+ let desc = template.querySelector(".description");
+
+ title.setAttribute("data-l10n-id", product.title_string_id);
+ title.id = product.id;
+
+ desc.setAttribute("data-l10n-id", product.description_string_id);
+
+ let isLink = product.button.type === "link";
+ let actionElement = template.querySelector(
+ isLink ? ".text-link" : ".small-button"
+ );
+
+ if (actionElement) {
+ actionElement.hidden = false;
+ actionElement.id = `${this.option}-${product.button.id}`;
+ document.l10n.setAttributes(
+ actionElement,
+ product.button.label_string_id
+ );
+
+ if (isLink) {
+ actionElement.setAttribute(
+ "href",
+ this.getURL(product.button.actionURL, product.region, this.option)
+ );
+ } else {
+ actionElement.addEventListener("click", function () {
+ let mainWindow = window.windowRoot.ownerGlobal;
+ mainWindow.openTrustedLinkIn(
+ gMoreFromMozillaPane.getURL(
+ product.button.actionURL,
+ product.region,
+ gMoreFromMozillaPane.option
+ ),
+ "tab"
+ );
+ });
+ }
+ }
+
+ if (product.qrcode) {
+ let qrcode = template.querySelector(".qr-code-box");
+ qrcode.setAttribute("hidden", "false");
+
+ let qrcode_title = template.querySelector(".qr-code-box-title");
+ qrcode_title.setAttribute(
+ "data-l10n-id",
+ product.qrcode.title.string_id
+ );
+
+ let img = template.querySelector(".qr-code-box-image");
+ // Append QRCode image source by template. For CN region
+ // simple template, we want a CN specific QRCode
+ img.src =
+ product.qrcode.image_src_prefix +
+ "-" +
+ this.getTemplateName() +
+ `${
+ AppConstants.isChinaRepack() &&
+ this.getTemplateName().includes("simple")
+ ? "-cn"
+ : ""
+ }` +
+ ".svg";
+ // Add image a11y attributes
+ img.setAttribute(
+ "data-l10n-id",
+ "more-from-moz-qr-code-firefox-mobile-img"
+ );
+
+ let qrc_link = template.querySelector(".qr-code-link");
+
+ // So the telemetry includes info about which option is being used
+ qrc_link.id = `${this.option}-${product.qrcode.button.id}`;
+
+ // For supported locales, this link allows users to send themselves a
+ // download link by email. It should be hidden for unsupported locales.
+ if (BrowserUtils.sendToDeviceEmailsSupported()) {
+ qrc_link.setAttribute(
+ "data-l10n-id",
+ product.qrcode.button.label.string_id
+ );
+ qrc_link.href = this.getURL(
+ product.qrcode.button.actionURL,
+ product.region,
+ this.option,
+ true
+ );
+ qrc_link.hidden = false;
+ }
+ }
+
+ frag.appendChild(template);
+ }
+ this._productsContainer.appendChild(frag);
+ },
+
+ async init() {
+ if (this.initialized) {
+ return;
+ }
+ this.initialized = true;
+ document
+ .getElementById("moreFromMozillaCategory")
+ .removeAttribute("data-hidden-from-search");
+ document
+ .getElementById("moreFromMozillaCategory-header")
+ .removeAttribute("data-hidden-from-search");
+
+ this.renderProducts();
+ },
+};
diff --git a/browser/components/preferences/moz.build b/browser/components/preferences/moz.build
new file mode 100644
index 0000000000..54f7b042fb
--- /dev/null
+++ b/browser/components/preferences/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ["dialogs"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser.ini", "tests/siteData/browser.ini"]
+
+for var in ("MOZ_APP_NAME", "MOZ_MACBUNDLE_NAME"):
+ DEFINES[var] = CONFIG[var]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"):
+ DEFINES["HAVE_SHELL_SERVICE"] = 1
+
+if CONFIG["MOZ_UPDATE_AGENT"]:
+ DEFINES["MOZ_UPDATE_AGENT"] = True
+
+JAR_MANIFESTS += ["jar.mn"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Settings UI")
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
new file mode 100644
index 0000000000..8498a5cfb7
--- /dev/null
+++ b/browser/components/preferences/preferences.js
@@ -0,0 +1,661 @@
+/* - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Import globals from the files imported by the .xul files.
+/* import-globals-from main.js */
+/* import-globals-from home.js */
+/* import-globals-from search.js */
+/* import-globals-from containers.js */
+/* import-globals-from privacy.js */
+/* import-globals-from sync.js */
+/* import-globals-from experimental.js */
+/* import-globals-from moreFromMozilla.js */
+/* import-globals-from findInPage.js */
+/* import-globals-from /browser/base/content/utilityOverlay.js */
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+var { Integration } = ChromeUtils.importESModule(
+ "resource://gre/modules/Integration.sys.mjs"
+);
+/* global DownloadIntegration */
+Integration.downloads.defineESModuleGetter(
+ this,
+ "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+var { PrivateBrowsingUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+);
+
+var { Weave } = ChromeUtils.importESModule(
+ "resource://services-sync/main.sys.mjs"
+);
+
+var { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
+ "resource://gre/modules/FirefoxRelayTelemetry.mjs"
+);
+
+var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+var fxAccounts = getFxAccountsSingleton();
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ gApplicationUpdateService: [
+ "@mozilla.org/updates/update-service;1",
+ "nsIApplicationUpdateService",
+ ],
+
+ listManager: [
+ "@mozilla.org/url-classifier/listmanager;1",
+ "nsIUrlListManager",
+ ],
+ gHandlerService: [
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService",
+ ],
+ gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
+});
+
+if (Cc["@mozilla.org/gio-service;1"]) {
+ XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gGIOService",
+ "@mozilla.org/gio-service;1",
+ "nsIGIOService"
+ );
+} else {
+ this.gGIOService = null;
+}
+
+ChromeUtils.defineESModuleGetters(this, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+ ExtensionPreferencesManager:
+ "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+ FeatureGate: "resource://featuregates/FeatureGate.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs",
+ LangPackMatcher: "resource://gre/modules/LangPackMatcher.sys.mjs",
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
+ UIState: "resource://services-sync/UIState.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.jsm",
+ SelectionChangedMenulist: "resource:///modules/SelectionChangedMenulist.jsm",
+ SiteDataManager: "resource:///modules/SiteDataManager.jsm",
+ TransientPrefs: "resource:///modules/TransientPrefs.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () {
+ const { SubDialogManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/SubDialog.sys.mjs"
+ );
+ return new SubDialogManager({
+ dialogStack: document.getElementById("dialogStack"),
+ dialogTemplate: document.getElementById("dialogTemplate"),
+ dialogOptions: {
+ styleSheets: [
+ "chrome://browser/skin/preferences/dialog.css",
+ "chrome://browser/skin/preferences/preferences.css",
+ ],
+ resizeCallback: async ({ title, frame }) => {
+ // Search within main document and highlight matched keyword.
+ await gSearchResultsPane.searchWithinNode(
+ title,
+ gSearchResultsPane.query
+ );
+
+ // Search within sub-dialog document and highlight matched keyword.
+ await gSearchResultsPane.searchWithinNode(
+ frame.contentDocument.firstElementChild,
+ gSearchResultsPane.query
+ );
+
+ // Creating tooltips for all the instances found
+ for (let node of gSearchResultsPane.listSearchTooltips) {
+ if (!node.tooltipNode) {
+ gSearchResultsPane.createSearchTooltip(
+ node,
+ gSearchResultsPane.query
+ );
+ }
+ }
+ },
+ },
+ });
+});
+
+var gLastCategory = { category: undefined, subcategory: undefined };
+const gXULDOMParser = new DOMParser();
+var gCategoryModules = new Map();
+var gCategoryInits = new Map();
+function init_category_if_required(category) {
+ let categoryInfo = gCategoryInits.get(category);
+ if (!categoryInfo) {
+ throw new Error(
+ "Unknown in-content prefs category! Can't init " + category
+ );
+ }
+ if (categoryInfo.inited) {
+ return null;
+ }
+ return categoryInfo.init();
+}
+
+function register_module(categoryName, categoryObject) {
+ gCategoryModules.set(categoryName, categoryObject);
+ gCategoryInits.set(categoryName, {
+ inited: false,
+ async init() {
+ let startTime = performance.now();
+ let template = document.getElementById("template-" + categoryName);
+ if (template) {
+ // Replace the template element with the nodes inside of it.
+ let frag = template.content;
+ await document.l10n.translateFragment(frag);
+
+ // Actually insert them into the DOM.
+ document.l10n.pauseObserving();
+ template.replaceWith(frag);
+ document.l10n.resumeObserving();
+
+ // We need to queue an update again because the previous update might
+ // have happened while we awaited on translateFragment.
+ Preferences.queueUpdateOfAllElements();
+ }
+
+ categoryObject.init();
+ this.inited = true;
+ ChromeUtils.addProfilerMarker(
+ "Preferences",
+ { startTime },
+ categoryName + " init"
+ );
+ },
+ });
+}
+
+document.addEventListener("DOMContentLoaded", init_all, { once: true });
+
+function init_all() {
+ Preferences.forceEnableInstantApply();
+
+ // Asks Preferences to queue an update of the attribute values of
+ // the entire document.
+ Preferences.queueUpdateOfAllElements();
+ Services.telemetry.setEventRecordingEnabled("aboutpreferences", true);
+
+ register_module("paneGeneral", gMainPane);
+ register_module("paneHome", gHomePane);
+ register_module("paneSearch", gSearchPane);
+ register_module("panePrivacy", gPrivacyPane);
+ register_module("paneContainers", gContainersPane);
+ if (Services.prefs.getBoolPref("browser.preferences.experimental")) {
+ // Set hidden based on previous load's hidden value.
+ document.getElementById("category-experimental").hidden =
+ Services.prefs.getBoolPref(
+ "browser.preferences.experimental.hidden",
+ false
+ );
+ register_module("paneExperimental", gExperimentalPane);
+ }
+
+ NimbusFeatures.moreFromMozilla.recordExposureEvent({ once: true });
+ if (NimbusFeatures.moreFromMozilla.getVariable("enabled")) {
+ document.getElementById("category-more-from-mozilla").hidden = false;
+ gMoreFromMozillaPane.option =
+ NimbusFeatures.moreFromMozilla.getVariable("template");
+ register_module("paneMoreFromMozilla", gMoreFromMozillaPane);
+ }
+ // The Sync category needs to be the last of the "real" categories
+ // registered and inititalized since many tests wait for the
+ // "sync-pane-loaded" observer notification before starting the test.
+ if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+ document.getElementById("category-sync").hidden = false;
+ register_module("paneSync", gSyncPane);
+ }
+ register_module("paneSearchResults", gSearchResultsPane);
+ gSearchResultsPane.init();
+ gMainPane.preInit();
+
+ let categories = document.getElementById("categories");
+ categories.addEventListener("select", event => gotoPref(event.target.value));
+
+ document.documentElement.addEventListener("keydown", function (event) {
+ if (event.keyCode == KeyEvent.DOM_VK_TAB) {
+ categories.setAttribute("keyboard-navigation", "true");
+ }
+ });
+ categories.addEventListener("mousedown", function () {
+ this.removeAttribute("keyboard-navigation");
+ });
+
+ maybeDisplayPoliciesNotice();
+
+ window.addEventListener("hashchange", onHashChange);
+
+ document.getElementById("focusSearch1").addEventListener("command", () => {
+ gSearchResultsPane.searchInput.focus();
+ });
+
+ gotoPref().then(() => {
+ document.getElementById("addonsButton").addEventListener("click", e => {
+ e.preventDefault();
+ if (e.button >= 2) {
+ // Ignore right clicks.
+ return;
+ }
+ let mainWindow = window.browsingContext.topChromeWindow;
+ mainWindow.BrowserOpenAddonsMgr();
+ });
+
+ document.dispatchEvent(
+ new CustomEvent("Initialized", {
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+ });
+}
+
+function telemetryBucketForCategory(category) {
+ category = category.toLowerCase();
+ switch (category) {
+ case "containers":
+ case "general":
+ case "home":
+ case "privacy":
+ case "search":
+ case "sync":
+ case "searchresults":
+ return category;
+ default:
+ return "unknown";
+ }
+}
+
+function onHashChange() {
+ gotoPref(null, "hash");
+}
+
+async function gotoPref(
+ aCategory,
+ aShowReason = aCategory ? "click" : "initial"
+) {
+ let categories = document.getElementById("categories");
+ const kDefaultCategoryInternalName = "paneGeneral";
+ const kDefaultCategory = "general";
+ let hash = document.location.hash;
+ let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
+
+ let breakIndex = category.indexOf("-");
+ // Subcategories allow for selecting smaller sections of the preferences
+ // until proper search support is enabled (bug 1353954).
+ let subcategory = breakIndex != -1 && category.substring(breakIndex + 1);
+ if (subcategory) {
+ category = category.substring(0, breakIndex);
+ }
+ category = friendlyPrefCategoryNameToInternalName(category);
+ if (category != "paneSearchResults") {
+ gSearchResultsPane.query = null;
+ gSearchResultsPane.searchInput.value = "";
+ gSearchResultsPane.removeAllSearchIndicators(window, true);
+ } else if (!gSearchResultsPane.searchInput.value) {
+ // Something tried to send us to the search results pane without
+ // a query string. Default to the General pane instead.
+ category = kDefaultCategoryInternalName;
+ document.location.hash = kDefaultCategory;
+ gSearchResultsPane.query = null;
+ }
+
+ // Updating the hash (below) or changing the selected category
+ // will re-enter gotoPref.
+ if (gLastCategory.category == category && !subcategory) {
+ return;
+ }
+
+ let item;
+ if (category != "paneSearchResults") {
+ // Hide second level headers in normal view
+ for (let element of document.querySelectorAll(".search-header")) {
+ element.hidden = true;
+ }
+
+ item = categories.querySelector(".category[value=" + category + "]");
+ if (!item || item.hidden) {
+ category = kDefaultCategoryInternalName;
+ item = categories.querySelector(".category[value=" + category + "]");
+ }
+ }
+
+ if (
+ gLastCategory.category ||
+ category != kDefaultCategoryInternalName ||
+ subcategory
+ ) {
+ let friendlyName = internalPrefCategoryNameToFriendlyName(category);
+ // Overwrite the hash, unless there is no hash and we're switching to the
+ // default category, e.g. by using the 'back' button after navigating to
+ // a different category.
+ if (
+ !(!document.location.hash && category == kDefaultCategoryInternalName)
+ ) {
+ document.location.hash = friendlyName;
+ }
+ }
+ // Need to set the gLastCategory before setting categories.selectedItem since
+ // the categories 'select' event will re-enter the gotoPref codepath.
+ gLastCategory.category = category;
+ gLastCategory.subcategory = subcategory;
+ if (item) {
+ categories.selectedItem = item;
+ } else {
+ categories.clearSelection();
+ }
+ window.history.replaceState(category, document.title);
+
+ try {
+ await init_category_if_required(category);
+ } catch (ex) {
+ console.error(
+ new Error(
+ "Error initializing preference category " + category + ": " + ex
+ )
+ );
+ throw ex;
+ }
+
+ // Bail out of this goToPref if the category
+ // or subcategory changed during async operation.
+ if (
+ gLastCategory.category !== category ||
+ gLastCategory.subcategory !== subcategory
+ ) {
+ return;
+ }
+
+ search(category, "data-category");
+
+ if (aShowReason != "initial") {
+ document.querySelector(".main-content").scrollTop = 0;
+ }
+
+ // Check to see if the category module wants to do any special
+ // handling of the subcategory - for example, opening a SubDialog.
+ //
+ // If not, just do a normal spotlight on the subcategory.
+ let categoryModule = gCategoryModules.get(category);
+ if (!categoryModule.handleSubcategory?.(subcategory)) {
+ spotlight(subcategory, category);
+ }
+
+ // Record which category is shown
+ Services.telemetry.recordEvent(
+ "aboutpreferences",
+ "show",
+ aShowReason,
+ category
+ );
+}
+
+function search(aQuery, aAttribute) {
+ let mainPrefPane = document.getElementById("mainPrefPane");
+ let elements = mainPrefPane.children;
+ for (let element of elements) {
+ // If the "data-hidden-from-search" is "true", the
+ // element will not get considered during search.
+ if (
+ element.getAttribute("data-hidden-from-search") != "true" ||
+ element.getAttribute("data-subpanel") == "true"
+ ) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.hidden = false;
+ } else {
+ element.hidden = true;
+ }
+ } else if (
+ element.getAttribute("data-hidden-from-search") == "true" &&
+ !element.hidden
+ ) {
+ element.hidden = true;
+ }
+ element.classList.remove("visually-hidden");
+ }
+
+ let keysets = mainPrefPane.getElementsByTagName("keyset");
+ for (let element of keysets) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", true);
+ }
+ }
+}
+
+async function spotlight(subcategory, category) {
+ let highlightedElements = document.querySelectorAll(".spotlight");
+ if (highlightedElements.length) {
+ for (let element of highlightedElements) {
+ element.classList.remove("spotlight");
+ }
+ }
+ if (subcategory) {
+ scrollAndHighlight(subcategory, category);
+ }
+}
+
+async function scrollAndHighlight(subcategory, category) {
+ let element = document.querySelector(`[data-subcategory="${subcategory}"]`);
+ if (!element) {
+ return;
+ }
+ let header = getClosestDisplayedHeader(element);
+
+ scrollContentTo(header);
+ element.classList.add("spotlight");
+}
+
+/**
+ * If there is no visible second level header it will return first level header,
+ * otherwise return second level header.
+ * @returns {Element} - The closest displayed header.
+ */
+function getClosestDisplayedHeader(element) {
+ let header = element.closest("groupbox");
+ let searchHeader = header.querySelector(".search-header");
+ if (
+ searchHeader &&
+ searchHeader.hidden &&
+ header.previousElementSibling.classList.contains("subcategory")
+ ) {
+ header = header.previousElementSibling;
+ }
+ return header;
+}
+
+function scrollContentTo(element) {
+ const STICKY_CONTAINER_HEIGHT =
+ document.querySelector(".sticky-container").clientHeight;
+ let mainContent = document.querySelector(".main-content");
+ let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT;
+ mainContent.scroll({
+ top,
+ behavior: "smooth",
+ });
+}
+
+function friendlyPrefCategoryNameToInternalName(aName) {
+ if (aName.startsWith("pane")) {
+ return aName;
+ }
+ return "pane" + aName.substring(0, 1).toUpperCase() + aName.substr(1);
+}
+
+// This function is duplicated inside of utilityOverlay.js's openPreferences.
+function internalPrefCategoryNameToFriendlyName(aName) {
+ return (aName || "").replace(/^pane./, function (toReplace) {
+ return toReplace[4].toLowerCase();
+ });
+}
+
+// Put up a confirm dialog with "ok to restart", "revert without restarting"
+// and "restart later" buttons and returns the index of the button chosen.
+// We can choose not to display the "restart later", or "revert" buttons,
+// altough the later still lets us revert by using the escape key.
+//
+// The constants are useful to interpret the return value of the function.
+const CONFIRM_RESTART_PROMPT_RESTART_NOW = 0;
+const CONFIRM_RESTART_PROMPT_CANCEL = 1;
+const CONFIRM_RESTART_PROMPT_RESTART_LATER = 2;
+async function confirmRestartPrompt(
+ aRestartToEnable,
+ aDefaultButtonIndex,
+ aWantRevertAsCancelButton,
+ aWantRestartLaterButton
+) {
+ let [
+ msg,
+ title,
+ restartButtonText,
+ noRestartButtonText,
+ restartLaterButtonText,
+ ] = await document.l10n.formatValues([
+ {
+ id: aRestartToEnable
+ ? "feature-enable-requires-restart"
+ : "feature-disable-requires-restart",
+ },
+ { id: "should-restart-title" },
+ { id: "should-restart-ok" },
+ { id: "cancel-no-restart-button" },
+ { id: "restart-later" },
+ ]);
+
+ // Set up the first (index 0) button:
+ let buttonFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING;
+
+ // Set up the second (index 1) button:
+ if (aWantRevertAsCancelButton) {
+ buttonFlags +=
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING;
+ } else {
+ noRestartButtonText = null;
+ buttonFlags +=
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
+ }
+
+ // Set up the third (index 2) button:
+ if (aWantRestartLaterButton) {
+ buttonFlags +=
+ Services.prompt.BUTTON_POS_2 * Services.prompt.BUTTON_TITLE_IS_STRING;
+ } else {
+ restartLaterButtonText = null;
+ }
+
+ switch (aDefaultButtonIndex) {
+ case 0:
+ buttonFlags += Services.prompt.BUTTON_POS_0_DEFAULT;
+ break;
+ case 1:
+ buttonFlags += Services.prompt.BUTTON_POS_1_DEFAULT;
+ break;
+ case 2:
+ buttonFlags += Services.prompt.BUTTON_POS_2_DEFAULT;
+ break;
+ default:
+ break;
+ }
+
+ let buttonIndex = Services.prompt.confirmEx(
+ window,
+ title,
+ msg,
+ buttonFlags,
+ restartButtonText,
+ noRestartButtonText,
+ restartLaterButtonText,
+ null,
+ {}
+ );
+
+ // If we have the second confirmation dialog for restart, see if the user
+ // cancels out at that point.
+ if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ if (cancelQuit.data) {
+ buttonIndex = CONFIRM_RESTART_PROMPT_CANCEL;
+ }
+ }
+ return buttonIndex;
+}
+
+// This function is used to append search keywords found
+// in the related subdialog to the button that will activate the subdialog.
+function appendSearchKeywords(aId, keywords) {
+ let element = document.getElementById(aId);
+ let searchKeywords = element.getAttribute("searchkeywords");
+ if (searchKeywords) {
+ keywords.push(searchKeywords);
+ }
+ element.setAttribute("searchkeywords", keywords.join(" "));
+}
+
+async function ensureScrollPadding() {
+ let stickyContainer = document.querySelector(".sticky-container");
+ let height = await window.browsingContext.topChromeWindow
+ .promiseDocumentFlushed(() => stickyContainer.clientHeight)
+ .catch(err => Cu.reportError); // Can reject if the window goes away.
+
+ // Make it a bit more, to ensure focus rectangles etc. don't get cut off.
+ // This being 8px causes us to end up with 90px if the policies container
+ // is not visible (the common case), which matches the CSS and thus won't
+ // cause a style change, repaint, or other changes.
+ height += 8;
+ stickyContainer
+ .closest(".main-content")
+ .style.setProperty("scroll-padding-top", height + "px");
+}
+
+function maybeDisplayPoliciesNotice() {
+ if (Services.policies.status == Services.policies.ACTIVE) {
+ document.getElementById("policies-container").removeAttribute("hidden");
+ ensureScrollPadding();
+ }
+}
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
new file mode 100644
index 0000000000..2976864c80
--- /dev/null
+++ b/browser/components/preferences/preferences.xhtml
@@ -0,0 +1,244 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/handlers.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/search.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/containers.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
+
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ role="document"
+ id="preferences-root">
+
+<head>
+ <!-- @CSP: We should remove 'unsafe-inline' from style-src, see Bug 1579160 -->
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" />
+
+ <title data-l10n-id="settings-page-title"></title>
+
+ <meta name="color-scheme" content="light dark" />
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="browser/browser.ftl"/>
+ <!-- Used by fontbuilder.js -->
+ <link rel="localization" href="browser/preferences/fonts.ftl"/>
+ <link rel="localization" href="browser/preferences/moreFromMozilla.ftl"/>
+ <link rel="localization" href="browser/preferences/preferences.ftl"/>
+ <link rel="localization" href="toolkit/branding/accounts.ftl"/>
+ <link rel="localization" href="toolkit/branding/brandings.ftl"/>
+ <link rel="localization" href="toolkit/featuregates/features.ftl"/>
+
+ <!-- Links below are only used for search-l10n-ids into subdialogs -->
+ <link rel="localization" href="browser/aboutDialog.ftl"/>
+ <link rel="localization" href="browser/preferences/addEngine.ftl"/>
+ <link rel="localization" href="browser/preferences/blocklists.ftl"/>
+ <link rel="localization" href="browser/preferences/clearSiteData.ftl"/>
+ <link rel="localization" href="browser/preferences/colors.ftl"/>
+ <link rel="localization" href="browser/preferences/connection.ftl"/>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl"/>
+ <link rel="localization" href="browser/preferences/languages.ftl"/>
+ <link rel="localization" href="browser/preferences/permissions.ftl"/>
+ <link rel="localization" href="browser/preferences/selectBookmark.ftl"/>
+ <link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
+ <link rel="localization" href="browser/sanitize.ftl"/>
+ <link rel="localization" href="browser/translations.ftl"/>
+ <link rel="localization" href="preview/firefoxSuggest.ftl"/>
+ <link rel="localization" href="security/certificates/certManager.ftl"/>
+ <link rel="localization" href="security/certificates/deviceManager.ftl"/>
+ <link rel="localization" href="toolkit/updates/history.ftl"/>
+
+ <link rel="shortcut icon" href="chrome://global/skin/icons/settings.svg"/>
+
+ <script src="chrome://browser/content/utilityOverlay.js"/>
+ <script src="chrome://global/content/preferencesBindings.js"/>
+ <script src="chrome://browser/content/preferences/preferences.js"/>
+ <script src="chrome://browser/content/preferences/extensionControlled.js"/>
+ <script src="chrome://browser/content/preferences/findInPage.js"/>
+ <script type="module" src="chrome://global/content/elements/moz-support-link.mjs"/>
+ <script src="chrome://browser/content/migration/migration-wizard.mjs" type="module"></script>
+ <script type="module" src="chrome://global/content/elements/moz-toggle.mjs"/>
+</head>
+
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="preferences-body">
+
+ <stringbundle id="pkiBundle"
+ src="chrome://pippki/locale/pippki.properties"/>
+ <stringbundle id="browserBundle"
+ src="chrome://browser/locale/browser.properties"/>
+
+ <stack id="preferences-stack" flex="1">
+ <hbox flex="1">
+
+ <vbox class="navigation">
+ <!-- category list -->
+ <richlistbox id="categories" data-l10n-id="category-list" data-l10n-attrs="aria-label">
+ <richlistitem id="category-general"
+ class="category"
+ value="paneGeneral"
+ helpTopic="prefs-main"
+ data-l10n-id="category-general"
+ data-l10n-attrs="tooltiptext"
+ align="center">
+ <image class="category-icon"/>
+ <label class="category-name" flex="1" data-l10n-id="pane-general-title"></label>
+ </richlistitem>
+
+ <richlistitem id="category-home"
+ class="category"
+ value="paneHome"
+ helpTopic="prefs-home"
+ data-l10n-id="category-home"
+ data-l10n-attrs="tooltiptext"
+ align="center">
+ <image class="category-icon"/>
+ <label class="category-name" flex="1" data-l10n-id="pane-home-title"></label>
+ </richlistitem>
+
+ <richlistitem id="category-search"
+ class="category"
+ value="paneSearch"
+ helpTopic="prefs-search"
+ data-l10n-id="category-search"
+ data-l10n-attrs="tooltiptext"
+ align="center">
+ <image class="category-icon"/>
+ <label class="category-name" flex="1" data-l10n-id="pane-search-title"></label>
+ </richlistitem>
+
+ <!-- hidden with CSS; this is only here to allow the containers pane to
+ be switched to using the URL or the "Settings..." button. -->
+ <richlistitem id="category-containers"
+ class="category"
+ value="paneContainers"
+ helpTopic="prefs-containers"/>
+
+ <richlistitem id="category-privacy"
+ class="category"
+ value="panePrivacy"
+ helpTopic="prefs-privacy"
+ data-l10n-id="category-privacy"
+ data-l10n-attrs="tooltiptext"
+ align="center">
+ <image class="category-icon"/>
+ <label class="category-name" flex="1" data-l10n-id="pane-privacy-title"></label>
+ </richlistitem>
+
+ <richlistitem id="category-sync"
+ class="category"
+ hidden="true"
+ value="paneSync"
+ helpTopic="prefs-weave"
+ data-l10n-id="category-sync3"
+ data-l10n-attrs="tooltiptext"
+ align="center">
+ <image class="category-icon"/>
+ <label class="category-name" flex="1" data-l10n-id="pane-sync-title3"></label>
+ </richlistitem>
+
+ <richlistitem id="category-experimental"
+ class="category"
+ hidden="true"
+ value="paneExperimental"
+ helpTopic="prefs-experimental"
+ data-l10n-id="category-experimental"
+ data-l10n-attrs="tooltiptext"
+ align="center">
+ <image class="category-icon"/>
+ <label class="category-name" flex="1" data-l10n-id="pane-experimental-title"></label>
+ </richlistitem>
+ <richlistitem id="category-more-from-mozilla"
+ class="category"
+ hidden="true"
+ data-l10n-id="more-from-moz-category"
+ data-l10n-attrs="tooltiptext"
+ value="paneMoreFromMozilla"
+ align="center">
+ <image class="category-icon"/>
+ <label class="category-name" flex="1" data-l10n-id="more-from-moz-title"></label>
+ </richlistitem>
+ </richlistbox>
+
+ <spacer flex="1"/>
+
+ <vbox class="sidebar-footer-list">
+ <html:a id="addonsButton" class="sidebar-footer-link" href="about:addons">
+ <image class="sidebar-footer-icon addons-icon"/>
+ <label class="sidebar-footer-label" flex="1" data-l10n-id="addons-button-label"></label>
+ </html:a>
+ <html:a id="helpButton" class="sidebar-footer-link" target="_blank"
+ is="moz-support-link" support-page="preferences">
+ <image class="sidebar-footer-icon help-icon"/>
+ <label class="sidebar-footer-label" flex="1" data-l10n-id="help-button-label"></label>
+ </html:a>
+ </vbox>
+ </vbox>
+
+ <keyset>
+ <key data-l10n-id="focus-search" key="" modifiers="accel" id="focusSearch1"/>
+ </keyset>
+
+ <vbox class="main-content" flex="1" align="start">
+ <vbox class="pane-container">
+ <hbox class="sticky-container">
+ <hbox class="sticky-inner-container" pack="end" align="start">
+ <hbox id="policies-container" class="info-box-container smaller-font-size" flex="1" hidden="true">
+ <hbox class="info-icon-container">
+ <html:img class="info-icon"></html:img>
+ </hbox>
+ <hbox align="center" flex="1">
+ <html:a href="about:policies" target="_blank" data-l10n-id="managed-notice"/>
+ </hbox>
+ </hbox>
+ <search-textbox
+ id="searchInput"
+ data-l10n-id="search-input-box2"
+ data-l10n-attrs="placeholder, style"
+ hidden="true"/>
+ </hbox>
+ </hbox>
+ <vbox id="mainPrefPane">
+#include searchResults.inc.xhtml
+#include main.inc.xhtml
+#include home.inc.xhtml
+#include search.inc.xhtml
+#include privacy.inc.xhtml
+#include containers.inc.xhtml
+#include sync.inc.xhtml
+#include experimental.inc.xhtml
+#include moreFromMozilla.inc.xhtml
+ </vbox>
+ </vbox>
+ </vbox>
+ </hbox>
+
+ <stack id="dialogStack" hidden="true"/>
+ <vbox id="dialogTemplate" class="dialogOverlay" align="center" pack="center" topmost="true" hidden="true">
+ <vbox class="dialogBox"
+ pack="end"
+ role="dialog"
+ aria-labelledby="dialogTitle">
+ <hbox class="dialogTitleBar" align="center">
+ <label class="dialogTitle" flex="1"/>
+ <button class="dialogClose close-icon"
+ data-l10n-id="close-button"/>
+ </hbox>
+ <browser class="dialogFrame"
+ autoscroll="false"
+ disablehistory="true"/>
+ </vbox>
+ </vbox>
+ </stack>
+
+ <html:dialog id="migrationWizardDialog"></html:dialog>
+</html:body>
+</html>
diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml
new file mode 100644
index 0000000000..aea4c26913
--- /dev/null
+++ b/browser/components/preferences/privacy.inc.xhtml
@@ -0,0 +1,1380 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!-- Privacy panel -->
+
+<script src="chrome://browser/content/preferences/privacy.js"/>
+<stringbundle id="signonBundle" src="chrome://passwordmgr/locale/passwordmgr.properties"/>
+<html:template id="template-panePrivacy">
+<hbox id="browserPrivacyCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="privacy-header"/>
+</hbox>
+
+<!-- Tracking / Content Blocking -->
+<groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" aria-describedby="contentBlockingDescription" class="highlighting-group">
+ <label id="contentBlockingHeader"><html:h2 data-l10n-id="content-blocking-enhanced-tracking-protection"/></label>
+ <vbox data-subcategory="trackingprotection">
+ <hbox align="start">
+ <image id="trackingProtectionShield"/>
+ <description class="description-with-side-element" flex="1">
+ <html:span id="contentBlockingDescription" class="tail-with-learn-more" data-l10n-id="content-blocking-section-top-level-description"></html:span>
+ <html:a is="moz-support-link"
+ id="contentBlockingLearnMore"
+ class="learnMore"
+ data-l10n-id="content-blocking-learn-more"
+ support-page="enhanced-tracking-protection"
+ />
+ </description>
+ <button id="trackingProtectionExceptions"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="tracking-manage-exceptions"
+ preference="pref.privacy.disable_button.tracking_protection_exceptions"
+ search-l10n-ids="
+ permissions-address,
+ permissions-disable-etp,
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-exceptions-etp-window2.title,
+ permissions-exceptions-manage-etp-desc,
+ "/>
+ </hbox>
+ <hbox id="fpiIncompatibilityWarning" class="info-box-container" hidden="true">
+ <vbox class="info-icon-container">
+ <html:img class="info-icon"></html:img>
+ </vbox>
+ <vbox flex="1">
+ <description>
+ <html:span data-l10n-id="content-blocking-fpi-incompatibility-warning"/>
+ </description>
+ </vbox>
+ </hbox>
+ <vbox id="contentBlockingCategories">
+ <radiogroup id="contentBlockingCategoryRadio"
+ preference="browser.contentblocking.category"
+ aria-labelledby="trackingProtectionMenuDesc">
+ <vbox id="contentBlockingOptionStandard" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <radio id="standardRadio"
+ value="standard"
+ data-l10n-id="enhanced-tracking-protection-setting-standard"
+ flex="1"/>
+ <button id="standardArrow"
+ is="highlightable-button"
+ class="arrowhead"
+ data-l10n-id="content-blocking-expand-section"
+ aria-expanded="false"/>
+ </hbox>
+ <vbox class="indent">
+ <description data-l10n-id="content-blocking-etp-standard-desc"></description>
+ <vbox class="privacy-extra-information">
+ <label class="content-blocking-extra-blocking-desc" data-l10n-id="content-blocking-etp-blocking-desc"/>
+ <vbox class="indent">
+ <hbox class="extra-information-label social-media-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-social-media-trackers"/>
+ </hbox>
+ <hbox class="extra-information-label cross-site-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-cookies-in-all-windows2"/>
+ </hbox>
+ <hbox class="extra-information-label third-party-tracking-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label all-third-party-cookies-private-windows-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies-private-windows"/>
+ </hbox>
+ <hbox class="extra-information-label third-party-tracking-cookies-plus-isolate-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies-plus-isolate"/>
+ </hbox>
+ <hbox class="extra-information-label pb-trackers-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-private-windows"/>
+ </hbox>
+ <hbox class="extra-information-label trackers-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-windows-tracking-content"/>
+ </hbox>
+ <hbox class="extra-information-label all-third-party-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label all-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label unvisited-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-unvisited-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label cryptominers-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cryptominers"/>
+ </hbox>
+ <hbox class="extra-information-label fingerprinters-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-fingerprinters"/>
+ </hbox>
+ </vbox>
+ <vbox id="etpStandardTCPBox" class="content-blocking-warning info-box-container">
+ <label class="content-blocking-warning-title" data-l10n-id="content-blocking-etp-standard-tcp-title"/>
+ <description>
+ <html:span class="tail-with-learn-more" data-l10n-id="content-blocking-etp-standard-tcp-rollout-description"></html:span>
+ <html:a is="moz-support-link"
+ id="tcp-learn-more-link"
+ class="learnMore"
+ data-l10n-id="content-blocking-etp-standard-tcp-rollout-learn-more"
+ support-page="total-cookie-protection"
+ />
+ </description>
+ </vbox>
+ <html:div class="content-blocking-warning info-box-container reload-tabs" hidden="true">
+ <html:div class="content-blocking-reload-desc-container">
+ <html:div class="info-icon-container">
+ <html:img class="info-icon"/>
+ </html:div>
+ <html:span data-l10n-id="content-blocking-reload-description"
+ class="content-blocking-reload-description" />
+ </html:div>
+ <button class="accessory-button reload-tabs-button primary"
+ is="highlightable-button"
+ data-l10n-id="content-blocking-reload-tabs-button"/>
+ </html:div>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="contentBlockingOptionStrict" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <radio id="strictRadio"
+ value="strict"
+ data-l10n-id="enhanced-tracking-protection-setting-strict"
+ flex="1"/>
+ <button id="strictArrow"
+ is="highlightable-button"
+ class="arrowhead"
+ data-l10n-id="content-blocking-expand-section"
+ aria-expanded="false"/>
+ </hbox>
+ <vbox class="indent">
+ <label data-l10n-id="content-blocking-etp-strict-desc"></label>
+ <vbox class="privacy-extra-information">
+ <label class="content-blocking-extra-blocking-desc" data-l10n-id="content-blocking-etp-blocking-desc"/>
+ <vbox class="indent">
+ <hbox class="extra-information-label social-media-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-social-media-trackers"/>
+ </hbox>
+ <hbox class="extra-information-label third-party-tracking-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label all-third-party-cookies-private-windows-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies-private-windows"/>
+ </hbox>
+ <hbox class="extra-information-label cross-site-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-cookies-in-all-windows2"/>
+ </hbox>
+ <hbox class="extra-information-label pb-trackers-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-private-windows"/>
+ </hbox>
+ <hbox class="extra-information-label trackers-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-windows-tracking-content"/>
+ </hbox>
+ <hbox class="extra-information-label all-third-party-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-cross-site-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label all-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-all-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label unvisited-cookies-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-unvisited-cookies"/>
+ </hbox>
+ <hbox class="extra-information-label third-party-tracking-cookies-plus-isolate-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cross-site-tracking-cookies-plus-isolate"/>
+ </hbox>
+ <hbox class="extra-information-label cryptominers-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-cryptominers"/>
+ </hbox>
+ <hbox class="extra-information-label fingerprinters-option" hidden="true">
+ <label class="content-blocking-label" data-l10n-id="content-blocking-fingerprinters"/>
+ </hbox>
+ </vbox>
+ <html:div class="content-blocking-warning info-box-container reload-tabs" hidden="true">
+ <html:div class="content-blocking-reload-desc-container">
+ <html:div class="info-icon-container">
+ <html:img class="info-icon"/>
+ </html:div>
+ <html:span data-l10n-id="content-blocking-reload-description"
+ class="content-blocking-reload-description" />
+ </html:div>
+ <button class="accessory-button reload-tabs-button primary"
+ is="highlightable-button"
+ data-l10n-id="content-blocking-reload-tabs-button"/>
+ </html:div>
+ <vbox class="content-blocking-warning info-box-container">
+ <hbox>
+ <image class="content-blocking-warning-image"/>
+ <label class="content-blocking-warning-title" data-l10n-id="content-blocking-warning-title"/>
+ </hbox>
+ <description class="indent">
+ <html:span class="tail-with-learn-more content-blocking-warning-description" data-l10n-id="content-blocking-and-isolating-etp-warning-description-2"></html:span>
+ <html:a is="moz-support-link"
+ class="learnMore"
+ data-l10n-id="content-blocking-warning-learn-how"
+ support-page="turn-off-etp-desktop"
+ />
+ </description>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="contentBlockingOptionCustom" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <radio id="customRadio"
+ value="custom"
+ data-l10n-id="enhanced-tracking-protection-setting-custom"
+ flex="1"/>
+ <button id="customArrow"
+ is="highlightable-button"
+ class="arrowhead"
+ data-l10n-id="content-blocking-expand-section"
+ aria-expanded="false"/>
+ </hbox>
+ <vbox class="indent">
+ <description id="contentBlockingCustomDesc" data-l10n-id="content-blocking-etp-custom-desc"></description>
+ <vbox class="privacy-extra-information">
+ <hbox class="reject-trackers-ui custom-option">
+ <checkbox id="contentBlockingBlockCookiesCheckbox"
+ class="content-blocking-checkbox" flex="1"
+ data-l10n-id="content-blocking-cookies-label"
+ aria-describedby="contentBlockingCustomDesc"
+ preference="network.cookie.cookieBehavior"/>
+ <vbox>
+ <menulist id="blockCookiesMenu"
+ sizetopopup="none"
+ preference="network.cookie.cookieBehavior">
+ <menupopup>
+ <menuitem id="blockCookiesSocialMedia" data-l10n-id="sitedata-option-block-cross-site-trackers" value="trackers"/>
+ <menuitem id="isolateCookiesSocialMedia" data-l10n-id="sitedata-option-block-cross-site-cookies" value="trackers-plus-isolate"/>
+ <menuitem data-l10n-id="sitedata-option-block-unvisited" value="unvisited"/>
+ <menuitem data-l10n-id="sitedata-option-block-all-cross-site-cookies" value="all-third-parties"/>
+ <menuitem data-l10n-id="sitedata-option-block-all" value="always"/>
+ </menupopup>
+ </menulist>
+ </vbox>
+ </hbox>
+ <hbox id="contentBlockingTrackingProtectionExtensionContentLabel"
+ align="center" hidden="true" class="extension-controlled">
+ <description control="contentBlockingDisableTrackingProtectionExtension" flex="1"/>
+ <button id="contentBlockingDisableTrackingProtectionExtension"
+ is="highlightable-button"
+ class="extension-controlled-button accessory-button"
+ data-l10n-id="disable-extension" hidden="true"/>
+ </hbox>
+ <hbox class="custom-option">
+ <checkbox id="contentBlockingTrackingProtectionCheckbox"
+ class="content-blocking-checkbox" flex="1"
+ data-l10n-id="content-blocking-tracking-content-label"
+ aria-describedby="contentBlockingCustomDesc"/>
+ <vbox>
+ <menulist id="trackingProtectionMenu">
+ <menupopup>
+ <menuitem data-l10n-id="content-blocking-option-private" value="private"/>
+ <menuitem data-l10n-id="content-blocking-tracking-protection-option-all-windows" value="always"/>
+ </menupopup>
+ </menulist>
+ </vbox>
+ </hbox>
+ <label id="changeBlockListLink"
+ data-l10n-id="content-blocking-tracking-protection-change-block-list"
+ is="text-link"
+ search-l10n-ids="blocklist-window2.title, blocklist-description, blocklist-dialog.buttonlabelaccept"/>
+
+ <hbox class="custom-option" id="contentBlockingCryptominersOption">
+ <checkbox id="contentBlockingCryptominersCheckbox"
+ class="content-blocking-checkbox" flex="1"
+ preference="privacy.trackingprotection.cryptomining.enabled"
+ data-l10n-id="content-blocking-cryptominers-label"
+ aria-describedby="contentBlockingCustomDesc"/>
+ </hbox>
+ <hbox class="custom-option" id="contentBlockingFingerprintersOption">
+ <checkbox id="contentBlockingFingerprintersCheckbox"
+ class="content-blocking-checkbox" flex="1"
+ preference="privacy.trackingprotection.fingerprinting.enabled"
+ data-l10n-id="content-blocking-fingerprinters-label"
+ aria-describedby="contentBlockingCustomDesc"/>
+ </hbox>
+ <html:div class="content-blocking-warning info-box-container reload-tabs" hidden="true">
+ <html:div class="content-blocking-reload-desc-container">
+ <html:div class="info-icon-container">
+ <html:img class="info-icon"/>
+ </html:div>
+ <html:span data-l10n-id="content-blocking-reload-description"
+ class="content-blocking-reload-description" />
+ </html:div>
+ <button class="accessory-button reload-tabs-button primary"
+ is="highlightable-button"
+ data-l10n-id="content-blocking-reload-tabs-button"/>
+ </html:div>
+ <vbox class="content-blocking-warning info-box-container">
+ <hbox>
+ <image class="content-blocking-warning-image"/>
+ <label class="content-blocking-warning-title" data-l10n-id="content-blocking-warning-title"/>
+ </hbox>
+ <description class="indent">
+ <html:span class="tail-with-learn-more content-blocking-warning-description" data-l10n-id="content-blocking-and-isolating-etp-warning-description-2"></html:span>
+ <html:a is="moz-support-link"
+ class="learnMore"
+ data-l10n-id="content-blocking-warning-learn-how"
+ support-page="turn-off-etp-desktop"
+ />
+ </description>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </radiogroup>
+ </vbox>
+ <vbox id="doNotTrackLearnMoreBox">
+ <label>
+ <label class="tail-with-learn-more" data-l10n-id="do-not-track-description" id="doNotTrackDesc"></label>
+ <html:a is="moz-support-link"
+ class="learnMore"
+ data-l10n-id="do-not-track-learn-more"
+ support-page="how-do-i-turn-do-not-track-feature"
+ />
+ </label>
+ <radiogroup id="doNotTrackRadioGroup" aria-labelledby="doNotTrackDesc" preference="privacy.donottrackheader.enabled">
+ <radio value="true" data-l10n-id="do-not-track-option-always"/>
+ <radio value="false" data-l10n-id="do-not-track-option-default-content-blocking-known"/>
+ </radiogroup>
+ </vbox>
+ </vbox>
+</groupbox>
+
+<!-- Site Data -->
+<groupbox id="siteDataGroup" data-category="panePrivacy" hidden="true" aria-describedby="totalSiteDataSize">
+ <label><html:h2 data-l10n-id="sitedata-header"/></label>
+
+ <hbox data-subcategory="sitedata" align="baseline">
+ <vbox flex="1">
+ <description class="description-with-side-element" flex="1">
+ <html:span id="totalSiteDataSize" class="tail-with-learn-more"></html:span>
+ <html:a is="moz-support-link"
+ id="siteDataLearnMoreLink"
+ data-l10n-id="sitedata-learn-more"
+ support-page="storage-permissions"
+ />
+ </description>
+ <hbox flex="1" id="deleteOnCloseNote" class="info-box-container smaller-font-size">
+ <hbox class="info-icon-container">
+ <html:img class="info-icon"></html:img>
+ </hbox>
+ <description flex="1" data-l10n-id="sitedata-delete-on-close-private-browsing" />
+ </hbox>
+ <hbox id="keepRow"
+ align="center">
+ <checkbox id="deleteOnClose"
+ data-l10n-id="sitedata-delete-on-close"
+ flex="1" />
+ </hbox>
+ </vbox>
+ <vbox align="end">
+ <button id="clearSiteDataButton"
+ is="highlightable-button"
+ class="accessory-button"
+ search-l10n-ids="clear-site-data-cookies-empty.label, clear-site-data-cache-empty.label"
+ data-l10n-id="sitedata-clear"/>
+ <button id="siteDataSettings"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="sitedata-settings"
+ search-l10n-ids="
+ site-data-settings-window.title,
+ site-data-column-host.label,
+ site-data-column-cookies.label,
+ site-data-column-storage.label,
+ site-data-settings-description,
+ site-data-remove-all.label,
+ "/>
+ <button id="cookieExceptions"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="sitedata-cookies-exceptions"
+ preference="pref.privacy.disable_button.cookie_exceptions"
+ search-l10n-ids="
+ permissions-address,
+ permissions-block.label,
+ permissions-allow.label,
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-exceptions-cookie-desc,
+ " />
+ </vbox>
+ </hbox>
+</groupbox>
+
+<!-- Cookie Banner Handling -->
+<groupbox id="cookieBannerHandlingGroup" data-category="panePrivacy" data-subcategory="cookiebanner" hidden="true">
+ <label><html:h2 data-l10n-id="cookie-banner-handling-header" /></label>
+ <vbox flex="1">
+ <hbox>
+ <description>
+ <html:span id="cookieBannerReductionExplanation" class="tail-with-learn-more" data-l10n-id="cookie-banner-handling-description" ></html:span>
+ <html:a is="moz-support-link" id="cookieBannerHandlingLearnMore" class="learnMore" data-l10n-id="cookie-banner-learn-more" support-page="cookie-banner-reduction"/>
+ </description>
+ </hbox>
+ <hbox>
+ <checkbox id="handleCookieBanners"
+ preference="cookiebanners.service.mode"
+ data-l10n-id="forms-handle-cookie-banners"
+ flex="1" />
+ </hbox>
+ </vbox>
+</groupbox>
+
+<!-- Passwords -->
+<groupbox id="passwordsGroup" orient="vertical" data-category="panePrivacy" data-subcategory="logins" hidden="true">
+ <label><html:h2 data-l10n-id="pane-privacy-logins-and-passwords-header" data-l10n-attrs="searchkeywords"/></label>
+
+ <vbox id="passwordSettings">
+ <hbox id="passwordManagerExtensionContent"
+ class="extension-controlled"
+ align="center"
+ hidden="true">
+ <description control="disablePasswordManagerExtension"
+ flex="1"/>
+ <button id="disablePasswordManagerExtension"
+ class="extension-controlled-button accessory-button"
+ data-l10n-id="disable-extension"
+ hidden="true" />
+ </hbox>
+ <hbox>
+ <vbox flex="1">
+ <hbox>
+ <checkbox id="savePasswords"
+ data-l10n-id="forms-ask-to-save-logins"
+ preference="signon.rememberSignons"
+ flex="1" />
+ </hbox>
+ <hbox class="indent" flex="1">
+ <checkbox id="passwordAutofillCheckbox"
+ data-l10n-id="forms-fill-logins-and-passwords"
+ search-l10n-id="forms-fill-logins-and-passwords.label"
+ preference="signon.autofillForms"
+ flex="1" />
+ </hbox>
+ <hbox class="indent" id="generatePasswordsBox" flex="1">
+ <checkbox id="generatePasswords"
+ data-l10n-id="forms-generate-passwords"
+ search-l10n-ids="forms-generate-passwords.label"
+ preference="signon.generation.enabled"
+ flex="1" />
+ </hbox>
+ </vbox>
+ <vbox align="end">
+ <button id="passwordExceptions"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="forms-exceptions"
+ preference="pref.privacy.disable_button.view_passwords_exceptions"
+ search-l10n-ids="
+ permissions-address,
+ permissions-exceptions-saved-logins-window2.title,
+ permissions-exceptions-saved-logins-desc,
+ "/>
+ <button id="showPasswords"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="forms-saved-logins"
+ search-l10n-ids="forms-saved-logins.label"
+ preference="pref.privacy.disable_button.view_passwords"/>
+ </vbox>
+ </hbox>
+ <hbox class="indent" id="relayIntegrationBox" flex="1" align="center">
+ <checkbox id="relayIntegration"
+ class="tail-with-learn-more"
+ data-l10n-id="preferences-relay-integration-checkbox"
+ search-l10n-ids="preferences-relay-integration-checkbox.label" />
+ <html:a id="relayIntegrationLearnMoreLink" class="learnMore"
+ data-l10n-id="relay-integration-learn-more-link"/>
+ </hbox>
+ <hbox class="indent" id="breachAlertsBox" flex="1" align="center">
+ <checkbox id="breachAlerts"
+ class="tail-with-learn-more"
+ data-l10n-id="forms-breach-alerts"
+ search-l10n-ids="breach-alerts.label"
+ preference="signon.management.page.breach-alerts.enabled"/>
+ <html:a is="moz-support-link"
+ id="breachAlertsLearnMoreLink"
+ data-l10n-id="forms-breach-alerts-learn-more-link"
+ support-page="lockwise-alerts"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <hbox id="masterPasswordRow" align="center">
+ <checkbox id="useMasterPassword"
+ data-l10n-id="forms-primary-pw-use"
+ class="tail-with-learn-more"/>
+ <html:a is="moz-support-link"
+ id="primaryPasswordLearnMoreLink"
+ data-l10n-id="forms-primary-pw-learn-more-link"
+ support-page="primary-password-stored-logins"
+ />
+ <spacer flex="1"/>
+ <button id="changeMasterPassword"
+ is="highlightable-button"
+ class="accessory-button"
+ search-l10n-ids="forms-master-pw-change.label"
+ data-l10n-id="forms-primary-pw-change"/>
+ </hbox>
+ <description class="indent tip-caption"
+ data-l10n-id="forms-primary-pw-former-name"
+ data-l10n-attrs="hidden"
+ flex="1"/>
+#ifdef XP_WIN
+ <hbox id="windows-sso" align="center">
+ <checkbox data-l10n-id="forms-windows-sso"
+ preference="network.http.windows-sso.enabled"
+ class="tail-with-learn-more"/>
+ <html:a is="moz-support-link"
+ id="windowsSSOLearnMoreLink"
+ data-l10n-id="forms-windows-sso-learn-more-link"
+ support-page="windows-sso"
+ />
+ </hbox>
+ <description id="windows-sso-caption" class="indent tip-caption"
+ data-l10n-id="forms-windows-sso-desc"/>
+
+#endif
+ </vbox>
+ <!--
+ Those two strings are meant to be invisible and will be used exclusively to provide
+ localization for an alert window.
+ -->
+ <label id="fips-title" hidden="true" data-l10n-id="forms-primary-pw-fips-title"></label>
+ <label id="fips-desc" hidden="true" data-l10n-id="forms-master-pw-fips-desc"></label>
+</groupbox>
+
+<!-- The form autofill section is inserted in to this box
+ after the form autofill extension has initialized. -->
+<groupbox id="formAutofillGroupBox"
+ data-category="panePrivacy"
+ data-subcategory="form-autofill" hidden="true"></groupbox>
+
+<!-- History -->
+<groupbox id="historyGroup" data-category="panePrivacy" hidden="true">
+ <label><html:h2 data-l10n-id="history-header"/></label>
+ <hbox align="center">
+ <label id="historyModeLabel"
+ control="historyMode"
+ data-l10n-id="history-remember-label"/>
+ <menulist id="historyMode">
+ <menupopup>
+ <menuitem data-l10n-id="history-remember-option-all"
+ value="remember"
+ search-l10n-ids="history-remember-description"/>
+ <menuitem data-l10n-id="history-remember-option-never"
+ value="dontremember"
+ search-l10n-ids="history-dontremember-description"/>
+ <menuitem data-l10n-id="history-remember-option-custom"
+ value="custom"
+ search-l10n-ids="
+ history-private-browsing-permanent.label,
+ history-remember-browser-option.label,
+ history-remember-search-option.label,
+ history-clear-on-close-option.label,
+ history-clear-on-close-settings.label"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox>
+ <deck id="historyPane" flex="1">
+ <vbox id="historyRememberPane">
+ <hbox align="center" flex="1">
+ <vbox flex="1">
+ <description
+ class="description-with-side-element"
+ data-l10n-id="history-remember-description"/>
+ </vbox>
+ </hbox>
+ </vbox>
+ <vbox id="historyDontRememberPane">
+ <hbox align="center" flex="1">
+ <vbox flex="1">
+ <description
+ class="description-with-side-element"
+ data-l10n-id="history-dontremember-description"/>
+ </vbox>
+ </hbox>
+ </vbox>
+ <vbox id="historyCustomPane">
+ <vbox>
+ <checkbox id="privateBrowsingAutoStart"
+ data-l10n-id="history-private-browsing-permanent"
+ preference="browser.privatebrowsing.autostart"/>
+ <vbox class="indent">
+ <checkbox id="rememberHistory"
+ data-l10n-id="history-remember-browser-option"
+ preference="places.history.enabled"/>
+ <checkbox id="rememberForms"
+ data-l10n-id="history-remember-search-option"
+ preference="browser.formfill.enable"/>
+ <hbox id="clearDataBox"
+ align="center">
+ <checkbox id="alwaysClear"
+ preference="privacy.sanitize.sanitizeOnShutdown"
+ data-l10n-id="history-clear-on-close-option"
+ flex="1" />
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </deck>
+ <vbox id="historyButtons" align="end">
+ <button id="clearHistoryButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="history-clear-button"/>
+ <button id="clearDataSettings"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="history-clear-on-close-settings"
+ search-l10n-ids="
+ clear-data-settings-label,
+ history-section-label,
+ item-history-and-downloads.label,
+ item-cookies.label,
+ item-active-logins.label,
+ item-cache.label,
+ item-form-search-history.label,
+ data-section-label,
+ item-site-settings.label,
+ item-offline-apps.label
+ "/>
+ </vbox>
+ </hbox>
+</groupbox>
+
+<!-- Address Bar -->
+<groupbox id="locationBarGroup"
+ data-category="panePrivacy"
+ hidden="true"
+ data-subcategory="locationBar">
+ <label><html:h2 id="locationBarGroupHeader" data-l10n-id="addressbar-header"/></label>
+ <label id="locationBarSuggestionLabel" data-l10n-id="addressbar-suggest"/>
+ <checkbox id="historySuggestion" data-l10n-id="addressbar-locbar-history-option"
+ preference="browser.urlbar.suggest.history"/>
+ <checkbox id="bookmarkSuggestion" data-l10n-id="addressbar-locbar-bookmarks-option"
+ preference="browser.urlbar.suggest.bookmark"/>
+ <checkbox id="openpageSuggestion" data-l10n-id="addressbar-locbar-openpage-option"
+ preference="browser.urlbar.suggest.openpage"/>
+ <checkbox id="topSitesSuggestion"
+ data-l10n-id="addressbar-locbar-shortcuts-option"
+ preference="browser.urlbar.suggest.topsites"/>
+ <checkbox id="enginesSuggestion" data-l10n-id="addressbar-locbar-engines-option"
+ preference="browser.urlbar.suggest.engines"/>
+ <hbox id="firefoxSuggestBestMatchContainer" align="center" hidden="true">
+ <checkbox id="firefoxSuggestBestMatch"
+ class="tail-with-learn-more"
+ data-l10n-id="addressbar-firefox-suggest-best-match-option"
+ preference="browser.urlbar.suggest.bestmatch"/>
+ <html:a is="moz-support-link"
+ id="firefoxSuggestBestMatchLearnMore"
+ class="learnMore"
+ data-l10n-id="addressbar-best-match-learn-more"
+ support-page="firefox-suggest"
+ />
+ </hbox>
+ <hbox id="quickActionsBox" align="center" hidden="true">
+ <checkbox id="enableQuickActions"
+ class="tail-with-learn-more"
+ data-l10n-id="addressbar-locbar-quickactions-option"
+ preference="browser.urlbar.suggest.quickactions" />
+ <html:a is="moz-support-link"
+ id="quickActionsLink"
+ data-l10n-id="addressbar-quickactions-learn-more"
+ support-page="quick-actions-firefox-search-bar"
+ />
+ </hbox>
+ <vbox id="firefoxSuggestContainer" hidden="true">
+ <vbox class="firefoxSuggestOptionBox">
+ <html:moz-toggle id="firefoxSuggestNonsponsoredToggle"
+ preference="browser.urlbar.suggest.quicksuggest.nonsponsored"
+ data-l10n-id="addressbar-firefox-suggest-nonsponsored"
+ data-l10n-attrs="label, description" />
+ </vbox>
+ <vbox class="firefoxSuggestOptionBox">
+ <html:moz-toggle id="firefoxSuggestSponsoredToggle"
+ preference="browser.urlbar.suggest.quicksuggest.sponsored"
+ data-l10n-id="addressbar-firefox-suggest-sponsored"
+ data-l10n-attrs="label, description" />
+ </vbox>
+ <vbox class="firefoxSuggestOptionBox">
+ <html:moz-toggle id="firefoxSuggestDataCollectionToggle"
+ preference="browser.urlbar.quicksuggest.dataCollection.enabled"
+ data-l10n-id="addressbar-firefox-suggest-data-collection"
+ data-l10n-attrs="label, description">
+ <html:a slot="support-link"
+ is="moz-support-link"
+ id="firefoxSuggestDataCollectionLearnMore"
+ class="learnMore firefoxSuggestLearnMore"
+ data-l10n-id="addressbar-locbar-firefox-suggest-learn-more"
+ support-page="firefox-suggest"
+ />
+ </html:moz-toggle>
+ </vbox>
+ <hbox id="firefoxSuggestInfoBox" class="info-box-container smaller-font-size" align="start"
+ hidden="true">
+ <html:img class="info-icon"/>
+ <description flex="1">
+ <html:span id="firefoxSuggestInfoText" class="tail-with-learn-more"/>
+ <html:a is="moz-support-link"
+ id="firefoxSuggestInfoBoxLearnMore"
+ class="learnMore firefoxSuggestLearnMore"
+ data-l10n-id="addressbar-locbar-firefox-suggest-learn-more"
+ support-page="firefox-suggest"
+ />
+ </description>
+ </hbox>
+ <hbox id="dismissedSuggestions" align="center">
+ <vbox flex="1">
+ <label data-l10n-id="addressbar-dismissed-suggestions-label"/>
+ <description class="tip-caption">
+ <html:span id="dismissedSuggestionsDescription"
+ class="tail-with-learn-more"
+ data-l10n-id="addressbar-restore-dismissed-suggestions-description"/>
+ <html:a is="moz-support-link"
+ id="dismissedSuggestionsLearnMore"
+ class="learnMore firefoxSuggestLearnMore"
+ data-l10n-id="addressbar-restore-dismissed-suggestions-learn-more"
+ support-page="firefox-suggest"
+ />
+ </description>
+ </vbox>
+ <button id="restoreDismissedSuggestions"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="addressbar-restore-dismissed-suggestions-button"
+ aria-describedby="dismissedSuggestionsDescription"/>
+ </hbox>
+ </vbox>
+ <label id="openSearchEnginePreferences" is="text-link"
+ data-l10n-id="addressbar-suggestions-settings"/>
+</groupbox>
+
+<hbox id="permissionsCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="permissions-header"/>
+</hbox>
+
+<!-- Permissions -->
+<groupbox id="permissionsGroup" data-category="panePrivacy" hidden="true" data-subcategory="permissions">
+ <label class="search-header" hidden="true"><html:h2 data-l10n-id="permissions-header"/></label>
+
+ <!-- The hbox around the buttons is to compute the search tooltip position properly -->
+ <vbox>
+ <hbox id="locationSettingsRow" align="center" role="group" aria-labelledby="locationPermissionsLabel">
+ <description flex="1">
+ <image class="geo-icon permission-icon" />
+ <label id="locationPermissionsLabel" data-l10n-id="permissions-location"/>
+ </description>
+ <hbox pack="end">
+ <button id="locationSettingsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-location-settings"
+ search-l10n-ids="
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-site-location-window2.title,
+ permissions-site-location-desc,
+ permissions-site-location-disable-label,
+ permissions-site-location-disable-desc,
+ " />
+ </hbox>
+ </hbox>
+
+ <hbox id="cameraSettingsRow" align="center" role="group" aria-labelledby="cameraPermissionsLabel">
+ <description flex="1">
+ <image class="camera-icon permission-icon" />
+ <label id="cameraPermissionsLabel" data-l10n-id="permissions-camera"/>
+ </description>
+ <hbox pack="end">
+ <button id="cameraSettingsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-camera-settings"
+ search-l10n-ids="
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-site-camera-window2.title,
+ permissions-site-camera-desc,
+ permissions-site-camera-disable-label,
+ permissions-site-camera-disable-desc,
+ " />
+ </hbox>
+ </hbox>
+
+ <hbox id="microphoneSettingsRow" align="center" role="group" aria-labelledby="microphonePermissionsLabel">
+ <description flex="1">
+ <image class="microphone-icon permission-icon" />
+ <label id="microphonePermissionsLabel" data-l10n-id="permissions-microphone"/>
+ </description>
+ <hbox pack="end">
+ <button id="microphoneSettingsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-microphone-settings"
+ search-l10n-ids="
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-site-microphone-window2.title,
+ permissions-site-microphone-desc,
+ permissions-site-microphone-disable-label,
+ permissions-site-microphone-disable-desc,
+ " />
+ </hbox>
+ </hbox>
+
+ <hbox id="speakerSettingsRow" align="center" role="group" aria-labelledby="speakerPermissionsLabel">
+ <description flex="1">
+ <image class="speaker-icon permission-icon" />
+ <label id="speakerPermissionsLabel" data-l10n-id="permissions-speaker"/>
+ </description>
+ <hbox pack="end">
+ <button id="speakerSettingsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-speaker-settings"
+ search-l10n-ids="
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-site-speaker-window.title,
+ permissions-site-speaker-desc,
+ " />
+ </hbox>
+ </hbox>
+
+ <hbox id="notificationSettingsRow" align="center" role="group" aria-labelledby="notificationPermissionsLabel">
+ <description flex="1">
+ <image class="desktop-notification-icon permission-icon" />
+ <label id="notificationPermissionsLabel"
+ class="tail-with-learn-more"
+ data-l10n-id="permissions-notification"/>
+ <html:a is="moz-support-link"
+ id="notificationPermissionsLearnMore"
+ class="learnMore"
+ data-l10n-id="permissions-notification-link"
+ support-page="push"
+ />
+ </description>
+ <hbox pack="end">
+ <button id="notificationSettingsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-notification-settings"
+ search-l10n-ids="
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-site-notification-window2.title,
+ permissions-site-notification-desc,
+ permissions-site-notification-disable-label,
+ permissions-site-notification-disable-desc,
+ " />
+ </hbox>
+ </hbox>
+
+ <vbox id="notificationsDoNotDisturbBox" hidden="true">
+ <checkbox id="notificationsDoNotDisturb" class="indent"/>
+ </vbox>
+
+ <hbox id="autoplaySettingsRow" align="center" role="group" aria-labelledby="autoplayPermissionsLabel">
+ <description flex="1">
+ <image class="autoplay-icon permission-icon" />
+ <label id="autoplayPermissionsLabel"
+ data-l10n-id="permissions-autoplay"/>
+ </description>
+ <hbox pack="end">
+ <button id="autoplaySettingsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-autoplay-settings"
+ search-l10n-ids="
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-site-autoplay-window2.title,
+ permissions-site-autoplay-desc,
+ " />
+ </hbox>
+ </hbox>
+
+ <hbox id="xrSettingsRow" align="center" role="group" aria-labelledby="xrPermissionsLabel">
+ <description flex="1">
+ <image class="xr-icon permission-icon" />
+ <label id="xrPermissionsLabel" data-l10n-id="permissions-xr"/>
+ </description>
+ <hbox pack="end">
+ <button id="xrSettingsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-xr-settings"
+ search-l10n-ids="
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-site-xr-window2.title,
+ permissions-site-xr-desc,
+ permissions-site-xr-disable-label,
+ permissions-site-xr-disable-desc,
+ " />
+ </hbox>
+ </hbox>
+ </vbox>
+
+ <separator />
+
+ <hbox data-subcategory="permissions-block-popups">
+ <checkbox id="popupPolicy" preference="dom.disable_open_during_load"
+ data-l10n-id="permissions-block-popups"
+ flex="1" />
+ <button id="popupPolicyButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-block-popups-exceptions-button"
+ data-l10n-attrs="searchkeywords"
+ search-l10n-ids="
+ permissions-address,
+ permissions-exceptions-popup-window2.title,
+ permissions-exceptions-popup-desc,
+ " />
+ </hbox>
+
+ <hbox id="addonInstallBox">
+ <checkbox id="warnAddonInstall"
+ data-l10n-id="permissions-addon-install-warning"
+ preference="xpinstall.whitelist.required"
+ flex="1" />
+ <button id="addonExceptions"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="permissions-addon-exceptions"
+ search-l10n-ids="
+ permissions-address,
+ permissions-allow.label,
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-exceptions-addons-window2.title,
+ permissions-exceptions-addons-desc,
+ " />
+ </hbox>
+
+</groupbox>
+
+<!-- Firefox Data Collection and Use -->
+<hbox id="dataCollectionCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="collection-header"/>
+</hbox>
+
+<groupbox id="dataCollectionGroup" data-category="panePrivacy" hidden="true">
+ <label class="search-header" hidden="true"><html:h2 data-l10n-id="collection-header"/></label>
+
+ <description>
+ <label class="tail-with-learn-more" data-l10n-id="collection-description"/>
+ <html:a id="dataCollectionPrivacyNotice"
+ class="learnMore"
+ data-l10n-id="collection-privacy-notice"/>
+ </description>
+#ifdef MOZ_DATA_REPORTING
+ <hbox id="telemetry-container" class="info-box-container smaller-font-size" hidden="true">
+ <hbox class="info-icon-container">
+ <html:img class="info-icon"></html:img>
+ </hbox>
+ <description>
+ <html:span id="telemetryDisabledDescription" class="tail-with-learn-more"
+ data-l10n-id="collection-health-report-telemetry-disabled"/>
+ <html:a is="moz-support-link"
+ id="telemetryDataDeletionLearnMore"
+ class="learnMore"
+ data-l10n-id="collection-health-report-telemetry-disabled-link"/>
+ </description>
+ </hbox>
+ <vbox data-subcategory="reports">
+ <description flex="1">
+ <checkbox id="submitHealthReportBox"
+ data-l10n-id="collection-health-report"
+ class="tail-with-learn-more"/>
+ <html:a id="FHRLearnMore"
+ class="learnMore"
+ data-l10n-id="collection-health-report-link"/>
+ <vbox class="indent">
+ <hbox align="center">
+ <checkbox id="addonRecommendationEnabled"
+ class="tail-with-learn-more"
+ data-l10n-id="addon-recommendations"/>
+ <html:a is="moz-support-link"
+ id="addonRecommendationLearnMore"
+ class="learnMore"
+ data-l10n-id="addon-recommendations-link"
+ support-page="personalized-addons"
+ />
+ </hbox>
+ </vbox>
+ </description>
+#ifndef MOZ_TELEMETRY_REPORTING
+ <description id="TelemetryDisabledDesc"
+ class="indent tip-caption" control="telemetryGroup"
+ data-l10n-id="collection-health-report-disabled"/>
+#endif
+
+#ifdef MOZ_NORMANDY
+ <hbox align="center">
+ <checkbox id="optOutStudiesEnabled"
+ class="tail-with-learn-more"
+ data-l10n-id="collection-studies"/>
+ <html:a id="viewShieldStudies"
+ href="about:studies"
+ useoriginprincipal="true"
+ class="learnMore"
+ data-l10n-id="collection-studies-link"/>
+ </hbox>
+#endif
+
+#ifdef MOZ_CRASHREPORTER
+ <hbox align="center" class="checkbox-row">
+ <html:input type="checkbox"
+ id="automaticallySubmitCrashesBox"
+ preference="browser.crashReports.unsubmittedCheck.autoSubmit2"/>
+ <label for="automaticallySubmitCrashesBox"
+ id="crashReporterLabel"
+ data-l10n-id="collection-backlogged-crash-reports-with-link">
+ <html:a data-l10n-name="crash-reports-link" id="crashReporterLearnMore" target="_blank"/>
+ </label>
+ </hbox>
+#endif
+ </vbox>
+#endif
+ <vbox id="privacySegmentationSection" data-subcategory="privacy-segmentation" hidden="true">
+ <label>
+ <html:h2 data-l10n-id="privacy-segmentation-section-header"/>
+ </label>
+ <label data-l10n-id="privacy-segmentation-section-description"/>
+ <radiogroup id="privacyDataFeatureRecommendationRadioGroup" preference="browser.dataFeatureRecommendations.enabled">
+ <radio id="privacyDataFeatureRecommendationEnabled"
+ data-l10n-id="privacy-segmentation-radio-off"
+ value="true"/>
+ <radio id="privacyDataFeatureRecommendationDisabled"
+ data-l10n-id="privacy-segmentation-radio-on"
+ value="false"/>
+ </radiogroup>
+ </vbox>
+</groupbox>
+
+<hbox id="securityCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="security-header"/>
+</hbox>
+
+<!-- addons, forgery (phishing) UI Security -->
+<groupbox id="browsingProtectionGroup" data-category="panePrivacy" hidden="true">
+ <label><html:h2 data-l10n-id="security-browsing-protection"/></label>
+ <hbox align = "center">
+ <checkbox id="enableSafeBrowsing"
+ data-l10n-id="security-enable-safe-browsing"
+ class="tail-with-learn-more"/>
+ <html:a is="moz-support-link"
+ id="enableSafeBrowsingLearnMore"
+ class="learnMore"
+ data-l10n-id="security-enable-safe-browsing-link"
+ support-page="phishing-malware"
+ />
+ </hbox>
+ <vbox class="indent">
+ <checkbox id="blockDownloads"
+ data-l10n-id="security-block-downloads"/>
+ <checkbox id="blockUncommonUnwanted"
+ data-l10n-id="security-block-uncommon-software"/>
+ </vbox>
+</groupbox>
+
+<!-- Certificates -->
+<groupbox id="certSelection" data-category="panePrivacy" hidden="true">
+ <label><html:h2 data-l10n-id="certs-header"/></label>
+ <hbox align="start">
+ <checkbox id="enableOCSP"
+ data-l10n-id="certs-enable-ocsp"
+ preference="security.OCSP.enabled"
+ flex="1" />
+ <vbox align="end">
+ <button id="viewCertificatesButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="certs-view"
+ preference="security.disable_button.openCertManager"
+ search-l10n-ids="
+ certmgr-tab-mine.label,
+ certmgr-tab-people.label,
+ certmgr-tab-servers.label,
+ certmgr-tab-ca.label,
+ certmgr-mine,
+ certmgr-people,
+ certmgr-server,
+ certmgr-ca,
+ certmgr-cert-name.label,
+ certmgr-token-name.label,
+ certmgr-view.label,
+ certmgr-export.label,
+ certmgr-delete.label
+ "/>
+ <button id="viewSecurityDevicesButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="certs-devices"
+ preference="security.disable_button.openDeviceManager"
+ search-l10n-ids="
+ devmgr-window.title,
+ devmgr-devlist.label,
+ devmgr-header-details.label,
+ devmgr-header-value.label,
+ devmgr-button-login.label,
+ devmgr-button-logout.label,
+ devmgr-button-changepw.label,
+ devmgr-button-load.label,
+ devmgr-button-unload.label
+ "/>
+ </vbox>
+ </hbox>
+</groupbox>
+
+<!-- HTTPS-ONLY Mode -->
+<groupbox id="httpsOnlyBox" data-category="panePrivacy" hidden="true">
+ <label><html:h2 data-l10n-id="httpsonly-header"/></label>
+ <vbox data-subcategory="httpsonly" flex="1">
+ <label id="httpsOnlyDescription" data-l10n-id="httpsonly-description"/>
+ <html:a is="moz-support-link"
+ id="httpsOnlyLearnMore"
+ class="learnMore"
+ data-l10n-id="httpsonly-learn-more"
+ support-page="https-only-prefs"
+ />
+ </vbox>
+ <hbox>
+ <radiogroup flex="1" id="httpsOnlyRadioGroup">
+ <radio id="httpsOnlyRadioEnabled"
+ data-l10n-id="httpsonly-radio-enabled"
+ value="enabled"/>
+ <radio id="httpsOnlyRadioEnabledPBM"
+ data-l10n-id="httpsonly-radio-enabled-pbm"
+ value="privateOnly"/>
+ <radio id="httpsOnlyRadioDisabled"
+ data-l10n-id="httpsonly-radio-disabled"
+ value="disabled"/>
+ </radiogroup>
+ <vbox>
+ <button id="httpsOnlyExceptionButton" is="highlightable-button" class="accessory-button" disabled="true"
+ data-l10n-id="sitedata-cookies-exceptions" search-l10n-ids="
+ permissions-address,
+ permissions-allow.label,
+ permissions-remove.label,
+ permissions-remove-all.label,
+ permissions-exceptions-https-only-desc,
+ " />
+ </vbox>
+ </hbox>
+</groupbox>
+
+<!-- DoH -->
+<hbox id="DoHCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="preferences-doh-header"/>
+</hbox>
+
+<groupbox id="dohBox" data-category="panePrivacy" data-subcategory="doh" hidden="true" class="highlighting-group">
+ <label class="search-header" searchkeywords="doh trr" hidden="true"><html:h2 data-l10n-id="preferences-doh-header"/></label>
+ <vbox flex="1">
+ <html:span id="dohDescription" class="tail-with-learn-more" data-l10n-id="preferences-doh-description"></html:span>
+ <html:a is="moz-support-link"
+ id="dohLearnMore"
+ class="learnMore"
+ support-page="dns-over-https"
+ />
+ </vbox>
+ <vbox id="dohStatusSection" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <label id="dohStatus" class="doh-status-label tail-with-learn-more"/>
+ <html:a is="moz-support-link"
+ id="dohStatusLearnMore"
+ class="learnMore"
+ support-page="doh-status"
+ hidden="true"/>
+ </hbox>
+ <label class="doh-status-label" id="dohResolver"/>
+ <label class="doh-status-label" id="dohSteeringStatus" data-l10n-id="preferences-doh-steering-status" hidden="true"/>
+ </vbox>
+ <hbox id="dohExceptionBox">
+ <label flex="1" data-l10n-id="preferences-doh-exceptions-description"/>
+ <button id="dohExceptionsButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="preferences-doh-manage-exceptions"
+ search-l10n-ids="
+ permissions-doh-entry-field,
+ permissions-doh-add-exception.label,
+ permissions-doh-remove.label,
+ permissions-doh-remove-all.label,
+ permissions-exceptions-doh-window.title,
+ permissions-exceptions-manage-doh-desc,
+ "/>
+ </hbox>
+ <vbox>
+ <label><html:h2 id="dohGroupMessage" data-l10n-id="preferences-doh-group-message"/></label>
+ <vbox id="dohCategories">
+ <radiogroup id="dohCategoryRadioGroup"
+ preference="network.trr.mode" aria-labelledby="dohGroupMessage">
+ <vbox id="dohOptionDefault" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <radio id="dohDefaultRadio"
+ value="0"
+ data-l10n-id="preferences-doh-setting-default"
+ flex="1"/>
+ <button id="dohDefaultArrow"
+ is="highlightable-button"
+ class="arrowhead doh-expand-section"
+ data-l10n-id="preferences-doh-expand-section"
+ aria-expanded="false"/>
+ </hbox>
+ <vbox class="indent">
+ <label data-l10n-id="preferences-doh-default-desc"></label>
+ <vbox class="privacy-extra-information">
+ <vbox class="indent">
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-default-detailed-desc-1"/>
+ </hbox>
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-default-detailed-desc-2"/>
+ </hbox>
+ <hbox class="extra-information-label">
+ <label class="doh-label tail-with-learn-more" data-l10n-id="preferences-doh-default-detailed-desc-3"/>
+ <html:a is="moz-support-link"
+ id="default-desc-3-learn-more"
+ class="learnMore"
+ support-page="doh-local-provider"
+ />
+ </hbox>
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-default-detailed-desc-4"/>
+ </hbox>
+ <hbox class="extra-information-label">
+ <label class="doh-label tail-with-learn-more" data-l10n-id="preferences-doh-default-detailed-desc-5"/>
+ <html:a is="moz-support-link"
+ id="default-desc-5-learn-more"
+ class="learnMore"
+ support-page="firefox-turn-off-secure-dns"
+ />
+ </hbox>
+ </vbox>
+ <hbox id="dohWarningBox1" class="extra-information-label" hidden="true">
+ <checkbox id="dohWarnCheckbox1"
+ flex="1"
+ data-l10n-id="preferences-doh-checkbox-warn"
+ preference="network.trr.display_fallback_warning"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="dohOptionEnabled" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <radio id="dohEnabledRadio"
+ value="2"
+ data-l10n-id="preferences-doh-setting-enabled"
+ flex="1"/>
+ <button id="dohEnabledArrow"
+ is="highlightable-button"
+ class="arrowhead doh-expand-section"
+ data-l10n-id="preferences-doh-expand-section"
+ aria-expanded="false"/>
+ </hbox>
+ <vbox class="indent">
+ <label data-l10n-id="preferences-doh-enabled-desc"></label>
+ <vbox class="privacy-extra-information">
+ <vbox class="indent">
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-enabled-detailed-desc-1"/>
+ </hbox>
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-enabled-detailed-desc-2"/>
+ </hbox>
+ </vbox>
+ <hbox id="dohWarningBox2" class="extra-information-label" hidden="true">
+ <checkbox id="dohWarnCheckbox2"
+ flex="1"
+ data-l10n-id="preferences-doh-checkbox-warn"
+ preference="network.trr.display_fallback_warning"/>
+ </hbox>
+ <vbox class="extra-information-label">
+ <label data-l10n-id="preferences-doh-select-resolver"/>
+ <menulist id="dohEnabledResolverChoices"
+ sizetopopup="none">
+ </menulist>
+ <html:input id="dohEnabledInputField" type="text" style="flex: 1;"
+ preference="network.trr.custom_uri" hidden="true"/>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="dohOptionStrict" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <radio id="dohStrictRadio"
+ value="3"
+ data-l10n-id="preferences-doh-setting-strict"
+ flex="1"/>
+ <button id="dohStrictArrow"
+ is="highlightable-button"
+ class="arrowhead doh-expand-section"
+ data-l10n-id="preferences-doh-expand-section"
+ aria-expanded="false"/>
+ </hbox>
+ <vbox class="indent">
+ <label data-l10n-id="preferences-doh-strict-desc"></label>
+ <vbox class="privacy-extra-information">
+ <vbox class="indent">
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-strict-detailed-desc-1"/>
+ </hbox>
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-strict-detailed-desc-2"/>
+ </hbox>
+ <hbox class="extra-information-label">
+ <label class="doh-label" data-l10n-id="preferences-doh-strict-detailed-desc-3"/>
+ </hbox>
+ </vbox>
+ <vbox class="extra-information-label">
+ <label data-l10n-id="preferences-doh-select-resolver"/>
+ <menulist id="dohStrictResolverChoices"
+ sizetopopup="none">
+ </menulist>
+ <html:input id="dohStrictInputField" type="text" style="flex: 1;"
+ preference="network.trr.custom_uri" hidden="true"/>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="dohOptionOff" class="privacy-detailedoption info-box-container">
+ <hbox>
+ <radio id="dohOffRadio"
+ value="5"
+ data-l10n-id="preferences-doh-setting-off"
+ flex="1"/>
+ </hbox>
+ <vbox class="indent">
+ <label data-l10n-id="preferences-doh-off-desc"></label>
+ </vbox>
+ </vbox>
+ </radiogroup>
+ </vbox>
+ </vbox>
+</groupbox>
+
+</html:template>
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
new file mode 100644
index 0000000000..c6784ea84a
--- /dev/null
+++ b/browser/components/preferences/privacy.js
@@ -0,0 +1,3358 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+
+const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+
+const TRACKING_PROTECTION_KEY = "websites.trackingProtectionMode";
+const TRACKING_PROTECTION_PREFS = [
+ "privacy.trackingprotection.enabled",
+ "privacy.trackingprotection.pbmode.enabled",
+];
+const CONTENT_BLOCKING_PREFS = [
+ "privacy.trackingprotection.enabled",
+ "privacy.trackingprotection.pbmode.enabled",
+ "network.cookie.cookieBehavior",
+ "privacy.trackingprotection.fingerprinting.enabled",
+ "privacy.trackingprotection.cryptomining.enabled",
+ "privacy.firstparty.isolate",
+ "privacy.trackingprotection.emailtracking.enabled",
+ "privacy.trackingprotection.emailtracking.pbmode.enabled",
+];
+
+const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST =
+ "browser.urlbar.quicksuggest.blockedDigests";
+const PREF_URLBAR_WEATHER_USER_ENABLED = "browser.urlbar.suggest.weather";
+
+/*
+ * Prefs that are unique to sanitizeOnShutdown and are not shared
+ * with the deleteOnClose mechanism like privacy.clearOnShutdown.cookies, -cache and -offlineApps
+ */
+const SANITIZE_ON_SHUTDOWN_PREFS_ONLY = [
+ "privacy.clearOnShutdown.history",
+ "privacy.clearOnShutdown.downloads",
+ "privacy.clearOnShutdown.sessions",
+ "privacy.clearOnShutdown.formdata",
+ "privacy.clearOnShutdown.siteSettings",
+];
+
+const PREF_OPT_OUT_STUDIES_ENABLED = "app.shield.optoutstudies.enabled";
+const PREF_NORMANDY_ENABLED = "app.normandy.enabled";
+
+const PREF_ADDON_RECOMMENDATIONS_ENABLED = "browser.discovery.enabled";
+
+const PREF_PASSWORD_GENERATION_AVAILABLE = "signon.generation.available";
+const { BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN } = Ci.nsICookieService;
+
+const PASSWORD_MANAGER_PREF_ID = "services.passwordSavingEnabled";
+
+XPCOMUtils.defineLazyGetter(this, "AlertsServiceDND", function () {
+ try {
+ let alertsService = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIAlertsDoNotDisturb);
+ // This will throw if manualDoNotDisturb isn't implemented.
+ alertsService.manualDoNotDisturb;
+ return alertsService;
+ } catch (ex) {
+ return undefined;
+ }
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "OS_AUTH_ENABLED",
+ "signon.management.page.os-auth.enabled",
+ true
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gIsFirstPartyIsolated",
+ "privacy.firstparty.isolate",
+ false
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ DoHConfigController: "resource:///modules/DoHConfig.sys.mjs",
+});
+
+Preferences.addAll([
+ // Content blocking / Tracking Protection
+ { id: "privacy.trackingprotection.enabled", type: "bool" },
+ { id: "privacy.trackingprotection.pbmode.enabled", type: "bool" },
+ { id: "privacy.trackingprotection.fingerprinting.enabled", type: "bool" },
+ { id: "privacy.trackingprotection.cryptomining.enabled", type: "bool" },
+ { id: "privacy.trackingprotection.emailtracking.enabled", type: "bool" },
+ {
+ id: "privacy.trackingprotection.emailtracking.pbmode.enabled",
+ type: "bool",
+ },
+
+ // Social tracking
+ { id: "privacy.trackingprotection.socialtracking.enabled", type: "bool" },
+ { id: "privacy.socialtracking.block_cookies.enabled", type: "bool" },
+
+ // Tracker list
+ { id: "urlclassifier.trackingTable", type: "string" },
+
+ // Button prefs
+ { id: "pref.privacy.disable_button.cookie_exceptions", type: "bool" },
+ { id: "pref.privacy.disable_button.view_cookies", type: "bool" },
+ { id: "pref.privacy.disable_button.change_blocklist", type: "bool" },
+ {
+ id: "pref.privacy.disable_button.tracking_protection_exceptions",
+ type: "bool",
+ },
+
+ // Location Bar
+ { id: "browser.urlbar.suggest.bestmatch", type: "bool" },
+ { id: "browser.urlbar.suggest.bookmark", type: "bool" },
+ { id: "browser.urlbar.suggest.history", type: "bool" },
+ { id: "browser.urlbar.suggest.openpage", type: "bool" },
+ { id: "browser.urlbar.suggest.topsites", type: "bool" },
+ { id: "browser.urlbar.suggest.engines", type: "bool" },
+ { id: "browser.urlbar.suggest.quicksuggest.nonsponsored", type: "bool" },
+ { id: "browser.urlbar.suggest.quicksuggest.sponsored", type: "bool" },
+ { id: "browser.urlbar.quicksuggest.dataCollection.enabled", type: "bool" },
+ { id: PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, type: "string" },
+ { id: PREF_URLBAR_WEATHER_USER_ENABLED, type: "bool" },
+
+ // History
+ { id: "places.history.enabled", type: "bool" },
+ { id: "browser.formfill.enable", type: "bool" },
+ { id: "privacy.history.custom", type: "bool" },
+
+ // Cookies
+ { id: "network.cookie.cookieBehavior", type: "int" },
+ { id: "network.cookie.blockFutureCookies", type: "bool" },
+ // Content blocking category
+ { id: "browser.contentblocking.category", type: "string" },
+ { id: "browser.contentblocking.features.strict", type: "string" },
+
+ // Clear Private Data
+ { id: "privacy.sanitize.sanitizeOnShutdown", type: "bool" },
+ { id: "privacy.sanitize.timeSpan", type: "int" },
+ { id: "privacy.clearOnShutdown.cookies", type: "bool" },
+ { id: "privacy.clearOnShutdown.cache", type: "bool" },
+ { id: "privacy.clearOnShutdown.offlineApps", type: "bool" },
+ { id: "privacy.clearOnShutdown.history", type: "bool" },
+ { id: "privacy.clearOnShutdown.downloads", type: "bool" },
+ { id: "privacy.clearOnShutdown.sessions", type: "bool" },
+ { id: "privacy.clearOnShutdown.formdata", type: "bool" },
+ { id: "privacy.clearOnShutdown.siteSettings", type: "bool" },
+
+ // Do not track
+ { id: "privacy.donottrackheader.enabled", type: "bool" },
+
+ // Media
+ { id: "media.autoplay.default", type: "int" },
+
+ // Popups
+ { id: "dom.disable_open_during_load", type: "bool" },
+ // Passwords
+ { id: "signon.rememberSignons", type: "bool" },
+ { id: "signon.generation.enabled", type: "bool" },
+ { id: "signon.autofillForms", type: "bool" },
+ { id: "signon.management.page.breach-alerts.enabled", type: "bool" },
+ { id: "signon.firefoxRelay.feature", type: "string" },
+
+ // Buttons
+ { id: "pref.privacy.disable_button.view_passwords", type: "bool" },
+ { id: "pref.privacy.disable_button.view_passwords_exceptions", type: "bool" },
+
+ /* Certificates tab
+ * security.default_personal_cert
+ * - a string:
+ * "Select Automatically" select a certificate automatically when a site
+ * requests one
+ * "Ask Every Time" present a dialog to the user so he can select
+ * the certificate to use on a site which
+ * requests one
+ */
+ { id: "security.default_personal_cert", type: "string" },
+
+ { id: "security.disable_button.openCertManager", type: "bool" },
+
+ { id: "security.disable_button.openDeviceManager", type: "bool" },
+
+ { id: "security.OCSP.enabled", type: "int" },
+
+ // Add-ons, malware, phishing
+ { id: "xpinstall.whitelist.required", type: "bool" },
+
+ { id: "browser.safebrowsing.malware.enabled", type: "bool" },
+ { id: "browser.safebrowsing.phishing.enabled", type: "bool" },
+
+ { id: "browser.safebrowsing.downloads.enabled", type: "bool" },
+
+ { id: "urlclassifier.malwareTable", type: "string" },
+
+ {
+ id: "browser.safebrowsing.downloads.remote.block_potentially_unwanted",
+ type: "bool",
+ },
+ { id: "browser.safebrowsing.downloads.remote.block_uncommon", type: "bool" },
+
+ // First-Party Isolation
+ { id: "privacy.firstparty.isolate", type: "bool" },
+
+ // HTTPS-Only
+ { id: "dom.security.https_only_mode", type: "bool" },
+ { id: "dom.security.https_only_mode_pbm", type: "bool" },
+
+ // Windows SSO
+ { id: "network.http.windows-sso.enabled", type: "bool" },
+
+ // Quick Actions
+ { id: "browser.urlbar.quickactions.showPrefs", type: "bool" },
+ { id: "browser.urlbar.suggest.quickactions", type: "bool" },
+
+ // Cookie Banner Handling
+ { id: "cookiebanners.ui.desktop.enabled", type: "bool" },
+ { id: "cookiebanners.service.mode", type: "int" },
+ { id: "cookiebanners.service.detectOnly", type: "bool" },
+
+ // DoH
+ { id: "network.trr.mode", type: "int" },
+ { id: "network.trr.uri", type: "string" },
+ { id: "network.trr.default_provider_uri", type: "string" },
+ { id: "network.trr.custom_uri", type: "string" },
+ { id: "network.trr_ui.show_fallback_warning_option", type: "bool" },
+ { id: "network.trr.display_fallback_warning", type: "bool" },
+ { id: "doh-rollout.disable-heuristics", type: "bool" },
+]);
+
+// Study opt out
+if (AppConstants.MOZ_DATA_REPORTING) {
+ Preferences.addAll([
+ // Preference instances for prefs that we need to monitor while the page is open.
+ { id: PREF_OPT_OUT_STUDIES_ENABLED, type: "bool" },
+ { id: PREF_ADDON_RECOMMENDATIONS_ENABLED, type: "bool" },
+ { id: PREF_UPLOAD_ENABLED, type: "bool" },
+ ]);
+}
+// Privacy segmentation section
+Preferences.add({
+ id: "browser.dataFeatureRecommendations.enabled",
+ type: "bool",
+});
+
+// Data Choices tab
+if (AppConstants.MOZ_CRASHREPORTER) {
+ Preferences.add({
+ id: "browser.crashReports.unsubmittedCheck.autoSubmit2",
+ type: "bool",
+ });
+}
+
+function setEventListener(aId, aEventType, aCallback) {
+ document
+ .getElementById(aId)
+ .addEventListener(aEventType, aCallback.bind(gPrivacyPane));
+}
+
+function setSyncFromPrefListener(aId, aCallback) {
+ Preferences.addSyncFromPrefListener(document.getElementById(aId), aCallback);
+}
+
+function setSyncToPrefListener(aId, aCallback) {
+ Preferences.addSyncToPrefListener(document.getElementById(aId), aCallback);
+}
+
+function dataCollectionCheckboxHandler({
+ checkbox,
+ pref,
+ matchPref = () => true,
+ isDisabled = () => false,
+}) {
+ function updateCheckbox() {
+ let collectionEnabled = Services.prefs.getBoolPref(
+ PREF_UPLOAD_ENABLED,
+ false
+ );
+
+ if (collectionEnabled && matchPref()) {
+ if (Services.prefs.getBoolPref(pref, false)) {
+ checkbox.setAttribute("checked", "true");
+ } else {
+ checkbox.removeAttribute("checked");
+ }
+ checkbox.setAttribute("preference", pref);
+ } else {
+ checkbox.removeAttribute("preference");
+ checkbox.removeAttribute("checked");
+ }
+
+ checkbox.disabled =
+ !collectionEnabled || Services.prefs.prefIsLocked(pref) || isDisabled();
+ }
+
+ Preferences.get(PREF_UPLOAD_ENABLED).on("change", updateCheckbox);
+ updateCheckbox();
+}
+
+// Sets the "Learn how" SUMO link in the Strict/Custom options of Content Blocking.
+function setUpContentBlockingWarnings() {
+ document.getElementById("fpiIncompatibilityWarning").hidden =
+ !gIsFirstPartyIsolated;
+}
+
+function initTCPStandardSection() {
+ let cookieBehaviorPref = Preferences.get("network.cookie.cookieBehavior");
+ let updateTCPSectionVisibilityState = () => {
+ document.getElementById("etpStandardTCPBox").hidden =
+ cookieBehaviorPref.value !=
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+ };
+
+ cookieBehaviorPref.on("change", updateTCPSectionVisibilityState);
+
+ updateTCPSectionVisibilityState();
+}
+
+var gPrivacyPane = {
+ _pane: null,
+
+ /**
+ * Whether the prompt to restart Firefox should appear when changing the autostart pref.
+ */
+ _shouldPromptForRestart: true,
+
+ /**
+ * Update the tracking protection UI to deal with extension control.
+ */
+ _updateTrackingProtectionUI() {
+ let cBPrefisLocked = CONTENT_BLOCKING_PREFS.some(pref =>
+ Services.prefs.prefIsLocked(pref)
+ );
+ let tPPrefisLocked = TRACKING_PROTECTION_PREFS.some(pref =>
+ Services.prefs.prefIsLocked(pref)
+ );
+
+ function setInputsDisabledState(isControlled) {
+ let tpDisabled = tPPrefisLocked || isControlled;
+ let disabled = cBPrefisLocked || isControlled;
+ let tpCheckbox = document.getElementById(
+ "contentBlockingTrackingProtectionCheckbox"
+ );
+ // Only enable the TP menu if Detect All Trackers is enabled.
+ document.getElementById("trackingProtectionMenu").disabled =
+ tpDisabled || !tpCheckbox.checked;
+ tpCheckbox.disabled = tpDisabled;
+
+ document.getElementById("standardRadio").disabled = disabled;
+ document.getElementById("strictRadio").disabled = disabled;
+ document
+ .getElementById("contentBlockingOptionStrict")
+ .classList.toggle("disabled", disabled);
+ document
+ .getElementById("contentBlockingOptionStandard")
+ .classList.toggle("disabled", disabled);
+ let arrowButtons = document.querySelectorAll("button.arrowhead");
+ for (let button of arrowButtons) {
+ button.disabled = disabled;
+ }
+
+ // Notify observers that the TP UI has been updated.
+ // This is needed since our tests need to be notified about the
+ // trackingProtectionMenu element getting disabled/enabled at the right time.
+ Services.obs.notifyObservers(window, "privacy-pane-tp-ui-updated");
+ }
+
+ let policy = Services.policies.getActivePolicies();
+ if (
+ policy &&
+ ((policy.EnableTrackingProtection &&
+ policy.EnableTrackingProtection.Locked) ||
+ (policy.Cookies && policy.Cookies.Locked))
+ ) {
+ setInputsDisabledState(true);
+ }
+ if (tPPrefisLocked) {
+ // An extension can't control this setting if either pref is locked.
+ hideControllingExtension(TRACKING_PROTECTION_KEY);
+ setInputsDisabledState(false);
+ } else {
+ handleControllingExtension(
+ PREF_SETTING_TYPE,
+ TRACKING_PROTECTION_KEY
+ ).then(setInputsDisabledState);
+ }
+ },
+
+ /**
+ * Hide the "Change Block List" link for trackers/tracking content in the
+ * custom Content Blocking/ETP panel. By default, it will not be visible.
+ */
+ _showCustomBlockList() {
+ let prefValue = Services.prefs.getBoolPref(
+ "browser.contentblocking.customBlockList.preferences.ui.enabled"
+ );
+ if (!prefValue) {
+ document.getElementById("changeBlockListLink").style.display = "none";
+ } else {
+ setEventListener("changeBlockListLink", "click", this.showBlockLists);
+ }
+ },
+
+ /**
+ * Set up handlers for showing and hiding controlling extension info
+ * for tracking protection.
+ */
+ _initTrackingProtectionExtensionControl() {
+ setEventListener(
+ "contentBlockingDisableTrackingProtectionExtension",
+ "command",
+ makeDisableControllingExtension(
+ PREF_SETTING_TYPE,
+ TRACKING_PROTECTION_KEY
+ )
+ );
+
+ let trackingProtectionObserver = {
+ observe(subject, topic, data) {
+ gPrivacyPane._updateTrackingProtectionUI();
+ },
+ };
+
+ for (let pref of TRACKING_PROTECTION_PREFS) {
+ Services.prefs.addObserver(pref, trackingProtectionObserver);
+ }
+ window.addEventListener("unload", () => {
+ for (let pref of TRACKING_PROTECTION_PREFS) {
+ Services.prefs.removeObserver(pref, trackingProtectionObserver);
+ }
+ });
+ },
+
+ _initQuickActionsSection() {
+ let showPref = Preferences.get("browser.urlbar.quickactions.showPrefs");
+ let showQuickActionsGroup = () => {
+ document.getElementById("quickActionsBox").hidden = !showPref.value;
+ };
+ showPref.on("change", showQuickActionsGroup);
+ showQuickActionsGroup();
+ },
+
+ syncFromHttpsOnlyPref() {
+ let httpsOnlyOnPref = Services.prefs.getBoolPref(
+ "dom.security.https_only_mode"
+ );
+ let httpsOnlyOnPBMPref = Services.prefs.getBoolPref(
+ "dom.security.https_only_mode_pbm"
+ );
+ let httpsOnlyRadioGroup = document.getElementById("httpsOnlyRadioGroup");
+ let httpsOnlyExceptionButton = document.getElementById(
+ "httpsOnlyExceptionButton"
+ );
+
+ if (httpsOnlyOnPref) {
+ httpsOnlyRadioGroup.value = "enabled";
+ httpsOnlyExceptionButton.disabled = false;
+ } else if (httpsOnlyOnPBMPref) {
+ httpsOnlyRadioGroup.value = "privateOnly";
+ httpsOnlyExceptionButton.disabled = true;
+ } else {
+ httpsOnlyRadioGroup.value = "disabled";
+ httpsOnlyExceptionButton.disabled = true;
+ }
+
+ if (
+ Services.prefs.prefIsLocked("dom.security.https_only_mode") ||
+ Services.prefs.prefIsLocked("dom.security.https_only_mode_pbm")
+ ) {
+ httpsOnlyRadioGroup.disabled = true;
+ }
+ },
+
+ syncToHttpsOnlyPref() {
+ let value = document.getElementById("httpsOnlyRadioGroup").value;
+ Services.prefs.setBoolPref(
+ "dom.security.https_only_mode_pbm",
+ value == "privateOnly"
+ );
+ Services.prefs.setBoolPref(
+ "dom.security.https_only_mode",
+ value == "enabled"
+ );
+ },
+
+ /**
+ * Init HTTPS-Only mode and corresponding prefs
+ */
+ initHttpsOnly() {
+ // Set radio-value based on the pref value
+ this.syncFromHttpsOnlyPref();
+
+ // Create event listener for when the user clicks
+ // on one of the radio buttons
+ setEventListener(
+ "httpsOnlyRadioGroup",
+ "command",
+ this.syncToHttpsOnlyPref
+ );
+ // Update radio-value when the pref changes
+ Preferences.get("dom.security.https_only_mode").on("change", () =>
+ this.syncFromHttpsOnlyPref()
+ );
+ Preferences.get("dom.security.https_only_mode_pbm").on("change", () =>
+ this.syncFromHttpsOnlyPref()
+ );
+ },
+
+ get dnsOverHttpsResolvers() {
+ let providers = DoHConfigController.currentConfig.providerList;
+ // if there's no default, we'll hold its position with an empty string
+ let defaultURI = DoHConfigController.currentConfig.fallbackProviderURI;
+ let defaultIndex = providers.findIndex(p => p.uri == defaultURI);
+ if (defaultIndex == -1 && defaultURI) {
+ // the default value for the pref isn't included in the resolvers list
+ // so we'll make a stub for it. Without an id, we'll have to use the url as the label
+ providers.unshift({ uri: defaultURI });
+ }
+ return providers;
+ },
+
+ updateDoHResolverList(mode) {
+ let resolvers = this.dnsOverHttpsResolvers;
+ let currentURI = Preferences.get("network.trr.uri").value;
+ if (!currentURI) {
+ currentURI = Preferences.get("network.trr.default_provider_uri").value;
+ }
+ let menu = document.getElementById(`${mode}ResolverChoices`);
+
+ let selectedIndex = currentURI
+ ? resolvers.findIndex(r => r.uri == currentURI)
+ : 0;
+ if (selectedIndex == -1) {
+ // select the last "Custom" item
+ selectedIndex = menu.itemCount - 1;
+ }
+ menu.selectedIndex = selectedIndex;
+
+ let customInput = document.getElementById(`${mode}InputField`);
+ customInput.hidden = menu.value != "custom";
+ },
+
+ populateDoHResolverList(mode) {
+ let resolvers = this.dnsOverHttpsResolvers;
+ let defaultURI = DoHConfigController.currentConfig.fallbackProviderURI;
+ let menu = document.getElementById(`${mode}ResolverChoices`);
+
+ // populate the DNS-Over-HTTPS resolver list
+ menu.removeAllItems();
+ for (let resolver of resolvers) {
+ let item = menu.appendItem(undefined, resolver.uri);
+ if (resolver.uri == defaultURI) {
+ document.l10n.setAttributes(
+ item,
+ "connection-dns-over-https-url-item-default",
+ {
+ name: resolver.UIName || resolver.uri,
+ }
+ );
+ } else {
+ item.label = resolver.UIName || resolver.uri;
+ }
+ }
+ let lastItem = menu.appendItem(undefined, "custom");
+ document.l10n.setAttributes(
+ lastItem,
+ "connection-dns-over-https-url-custom"
+ );
+
+ // set initial selection in the resolver provider picker
+ this.updateDoHResolverList(mode);
+
+ let customInput = document.getElementById(`${mode}InputField`);
+
+ function updateURIPref() {
+ if (customInput.value == "") {
+ // Setting the pref to empty string will make it have the default
+ // pref value which makes us fallback to using the default TRR
+ // resolver in network.trr.default_provider_uri.
+ // If the input is empty we set it to "(space)" which is essentially
+ // the same.
+ Services.prefs.setStringPref("network.trr.uri", " ");
+ } else {
+ Services.prefs.setStringPref("network.trr.uri", customInput.value);
+ }
+ }
+
+ menu.addEventListener("command", () => {
+ if (menu.value == "custom") {
+ customInput.hidden = false;
+ updateURIPref();
+ } else {
+ customInput.hidden = true;
+ Services.prefs.setStringPref("network.trr.uri", menu.value);
+ }
+ Services.telemetry.recordEvent(
+ "security.doh.settings",
+ "provider_choice",
+ "value",
+ menu.value
+ );
+
+ // Update other menu too.
+ let otherMode = mode == "dohEnabled" ? "dohStrict" : "dohEnabled";
+ let otherMenu = document.getElementById(`${otherMode}ResolverChoices`);
+ let otherInput = document.getElementById(`${otherMode}InputField`);
+ otherMenu.value = menu.value;
+ otherInput.hidden = otherMenu.value != "custom";
+ });
+
+ // Change the URL when you press ENTER in the input field it or loses focus
+ customInput.addEventListener("change", () => {
+ updateURIPref();
+ });
+ },
+
+ async updateDoHStatus() {
+ let trrURI = Services.dns.currentTrrURI;
+ let hostname = "";
+ try {
+ hostname = new URL(trrURI).hostname;
+ } catch (e) {
+ hostname = await document.l10n.formatValue("preferences-doh-bad-url");
+ }
+
+ let steering = document.getElementById("dohSteeringStatus");
+ steering.hidden = true;
+
+ let dohResolver = document.getElementById("dohResolver");
+ dohResolver.hidden = true;
+
+ let status = document.getElementById("dohStatus");
+
+ async function setStatus(localizedStringName, options) {
+ let opts = options || {};
+ let statusString = await document.l10n.formatValue(
+ localizedStringName,
+ opts
+ );
+ document.l10n.setAttributes(status, "preferences-doh-status", {
+ status: statusString,
+ });
+ }
+
+ function computeStatus() {
+ let mode = Services.dns.currentTrrMode;
+ let confirmationState = Services.dns.currentTrrConfirmationState;
+ if (
+ mode == Ci.nsIDNSService.MODE_TRRFIRST ||
+ mode == Ci.nsIDNSService.MODE_TRRONLY
+ ) {
+ switch (confirmationState) {
+ case Ci.nsIDNSService.CONFIRM_TRYING_OK:
+ case Ci.nsIDNSService.CONFIRM_OK:
+ case Ci.nsIDNSService.CONFIRM_DISABLED:
+ return "preferences-doh-status-active";
+ default:
+ return "preferences-doh-status-not-active";
+ }
+ }
+
+ return "preferences-doh-status-disabled";
+ }
+
+ let errReason = "";
+ let confirmationStatus = Services.dns.lastConfirmationStatus;
+ if (confirmationStatus != Cr.NS_OK) {
+ errReason = ChromeUtils.getXPCOMErrorName(confirmationStatus);
+ } else {
+ errReason = Services.dns.getTRRSkipReasonName(
+ Services.dns.lastConfirmationSkipReason
+ );
+ }
+ let statusLabel = computeStatus();
+ // setStatus will format and set the statusLabel asynchronously.
+ setStatus(statusLabel, { reason: errReason });
+ dohResolver.hidden = statusLabel == "preferences-doh-status-disabled";
+
+ let statusLearnMore = document.getElementById("dohStatusLearnMore");
+ statusLearnMore.hidden = statusLabel != "preferences-doh-status-not-active";
+
+ // No need to set the resolver name since we're not going to show it.
+ if (statusLabel == "preferences-doh-status-disabled") {
+ return;
+ }
+
+ function nameOrDomain() {
+ for (let resolver of DoHConfigController.currentConfig.providerList) {
+ if (resolver.uri == trrURI) {
+ return resolver.UIName || hostname || trrURI;
+ }
+ }
+
+ // Also check if this is a steering provider.
+ for (let resolver of DoHConfigController.currentConfig.providerSteering
+ .providerList) {
+ if (resolver.uri == trrURI) {
+ steering.hidden = false;
+ return resolver.UIName || hostname || trrURI;
+ }
+ }
+
+ return hostname;
+ }
+
+ let resolverNameOrDomain = nameOrDomain();
+ document.l10n.setAttributes(dohResolver, "preferences-doh-resolver", {
+ name: resolverNameOrDomain,
+ });
+ },
+
+ highlightDoHCategoryAndUpdateStatus() {
+ let value = Preferences.get("network.trr.mode").value;
+ let defaultOption = document.getElementById("dohOptionDefault");
+ let enabledOption = document.getElementById("dohOptionEnabled");
+ let strictOption = document.getElementById("dohOptionStrict");
+ let offOption = document.getElementById("dohOptionOff");
+ defaultOption.classList.remove("selected");
+ enabledOption.classList.remove("selected");
+ strictOption.classList.remove("selected");
+ offOption.classList.remove("selected");
+
+ switch (value) {
+ case Ci.nsIDNSService.MODE_NATIVEONLY:
+ defaultOption.classList.add("selected");
+ break;
+ case Ci.nsIDNSService.MODE_TRRFIRST:
+ enabledOption.classList.add("selected");
+ break;
+ case Ci.nsIDNSService.MODE_TRRONLY:
+ strictOption.classList.add("selected");
+ break;
+ case Ci.nsIDNSService.MODE_TRROFF:
+ offOption.classList.add("selected");
+ break;
+ default:
+ // The pref is set to a random value.
+ // This shouldn't happen, but let's make sure off is selected.
+ offOption.classList.add("selected");
+ document.getElementById("dohCategoryRadioGroup").selectedIndex = 3;
+ break;
+ }
+
+ // When the mode is set to 0 we need to clear the URI so
+ // doh-rollout can kick in.
+ if (value == Ci.nsIDNSService.MODE_NATIVEONLY) {
+ Services.prefs.clearUserPref("network.trr.uri");
+ Services.prefs.clearUserPref("doh-rollout.disable-heuristics");
+ }
+
+ gPrivacyPane.updateDoHStatus();
+ },
+
+ /**
+ * Init DoH corresponding prefs
+ */
+ initDoH() {
+ Services.telemetry.setEventRecordingEnabled("security.doh.settings", true);
+
+ setEventListener("dohDefaultArrow", "command", this.toggleExpansion);
+ setEventListener("dohEnabledArrow", "command", this.toggleExpansion);
+ setEventListener("dohStrictArrow", "command", this.toggleExpansion);
+
+ function modeButtonPressed(e) {
+ // Clicking the active mode again should not generate another event
+ if (
+ parseInt(e.target.value) == Preferences.get("network.trr.mode").value
+ ) {
+ return;
+ }
+ Services.telemetry.recordEvent(
+ "security.doh.settings",
+ "mode_changed",
+ "button",
+ e.target.id
+ );
+ }
+
+ setEventListener("dohDefaultRadio", "command", modeButtonPressed);
+ setEventListener("dohEnabledRadio", "command", modeButtonPressed);
+ setEventListener("dohStrictRadio", "command", modeButtonPressed);
+ setEventListener("dohOffRadio", "command", modeButtonPressed);
+
+ function warnCheckboxClicked(e) {
+ Services.telemetry.recordEvent(
+ "security.doh.settings",
+ "warn_checkbox",
+ "checkbox",
+ `${e.target.checked}`
+ );
+ }
+
+ setEventListener("dohWarnCheckbox1", "command", warnCheckboxClicked);
+ setEventListener("dohWarnCheckbox2", "command", warnCheckboxClicked);
+
+ this.populateDoHResolverList("dohEnabled");
+ this.populateDoHResolverList("dohStrict");
+
+ Preferences.get("network.trr.uri").on("change", () => {
+ gPrivacyPane.updateDoHResolverList("dohEnabled");
+ gPrivacyPane.updateDoHResolverList("dohStrict");
+ gPrivacyPane.updateDoHStatus();
+ });
+
+ // Update status box and hightlightling when the pref changes
+ Preferences.get("network.trr.mode").on(
+ "change",
+ gPrivacyPane.highlightDoHCategoryAndUpdateStatus
+ );
+ this.highlightDoHCategoryAndUpdateStatus();
+
+ Services.obs.addObserver(this, "network:trr-uri-changed");
+ Services.obs.addObserver(this, "network:trr-mode-changed");
+ Services.obs.addObserver(this, "network:trr-confirmation");
+ let unload = () => {
+ Services.obs.removeObserver(this, "network:trr-uri-changed");
+ Services.obs.removeObserver(this, "network:trr-mode-changed");
+ Services.obs.removeObserver(this, "network:trr-confirmation");
+ };
+ window.addEventListener("unload", unload, { once: true });
+
+ if (Preferences.get("network.trr_ui.show_fallback_warning_option").value) {
+ document.getElementById("dohWarningBox1").hidden = false;
+ document.getElementById("dohWarningBox2").hidden = false;
+ }
+
+ let uriPref = Services.prefs.getStringPref("network.trr.uri");
+ // If the value isn't one of the providers, we need to update the
+ // custom_uri pref to make sure the input box contains the correct URL.
+ if (uriPref && !this.dnsOverHttpsResolvers.some(e => e.uri == uriPref)) {
+ Services.prefs.setStringPref(
+ "network.trr.custom_uri",
+ Services.prefs.getStringPref("network.trr.uri")
+ );
+ }
+
+ if (Services.prefs.prefIsLocked("network.trr.mode")) {
+ document.getElementById("dohCategoryRadioGroup").disabled = true;
+ Services.prefs.setStringPref("network.trr.custom_uri", uriPref);
+ }
+ },
+
+ /**
+ * Sets up the UI for the number of days of history to keep, and updates the
+ * label of the "Clear Now..." button.
+ */
+ init() {
+ this._updateSanitizeSettingsButton();
+ this.initDeleteOnCloseBox();
+ this.syncSanitizationPrefsWithDeleteOnClose();
+ this.initializeHistoryMode();
+ this.updateHistoryModePane();
+ this.updatePrivacyMicroControls();
+ this.initAutoStartPrivateBrowsingReverter();
+
+ /* Initialize Content Blocking */
+ this.initContentBlocking();
+
+ this._showCustomBlockList();
+ this.trackingProtectionReadPrefs();
+ this.networkCookieBehaviorReadPrefs();
+ this._initTrackingProtectionExtensionControl();
+
+ Services.telemetry.setEventRecordingEnabled("pwmgr", true);
+
+ Preferences.get("privacy.trackingprotection.enabled").on(
+ "change",
+ gPrivacyPane.trackingProtectionReadPrefs.bind(gPrivacyPane)
+ );
+ Preferences.get("privacy.trackingprotection.pbmode.enabled").on(
+ "change",
+ gPrivacyPane.trackingProtectionReadPrefs.bind(gPrivacyPane)
+ );
+
+ // Watch all of the prefs that the new Cookies & Site Data UI depends on
+ Preferences.get("network.cookie.cookieBehavior").on(
+ "change",
+ gPrivacyPane.networkCookieBehaviorReadPrefs.bind(gPrivacyPane)
+ );
+ Preferences.get("browser.privatebrowsing.autostart").on(
+ "change",
+ gPrivacyPane.networkCookieBehaviorReadPrefs.bind(gPrivacyPane)
+ );
+ Preferences.get("privacy.firstparty.isolate").on(
+ "change",
+ gPrivacyPane.networkCookieBehaviorReadPrefs.bind(gPrivacyPane)
+ );
+
+ setEventListener(
+ "trackingProtectionExceptions",
+ "command",
+ gPrivacyPane.showTrackingProtectionExceptions
+ );
+
+ Preferences.get("privacy.sanitize.sanitizeOnShutdown").on(
+ "change",
+ gPrivacyPane._updateSanitizeSettingsButton.bind(gPrivacyPane)
+ );
+ Preferences.get("browser.privatebrowsing.autostart").on(
+ "change",
+ gPrivacyPane.updatePrivacyMicroControls.bind(gPrivacyPane)
+ );
+ setEventListener("historyMode", "command", function () {
+ gPrivacyPane.updateHistoryModePane();
+ gPrivacyPane.updateHistoryModePrefs();
+ gPrivacyPane.updatePrivacyMicroControls();
+ gPrivacyPane.updateAutostart();
+ });
+ setEventListener("clearHistoryButton", "command", function () {
+ let historyMode = document.getElementById("historyMode");
+ // Select "everything" in the clear history dialog if the
+ // user has set their history mode to never remember history.
+ gPrivacyPane.clearPrivateDataNow(historyMode.value == "dontremember");
+ });
+ setEventListener("openSearchEnginePreferences", "click", function (event) {
+ if (event.button == 0) {
+ gotoPref("search");
+ }
+ return false;
+ });
+ setEventListener(
+ "privateBrowsingAutoStart",
+ "command",
+ gPrivacyPane.updateAutostart
+ );
+ setEventListener(
+ "cookieExceptions",
+ "command",
+ gPrivacyPane.showCookieExceptions
+ );
+ setEventListener(
+ "httpsOnlyExceptionButton",
+ "command",
+ gPrivacyPane.showHttpsOnlyModeExceptions
+ );
+ setEventListener(
+ "dohExceptionsButton",
+ "command",
+ gPrivacyPane.showDoHExceptions
+ );
+ setEventListener(
+ "clearDataSettings",
+ "command",
+ gPrivacyPane.showClearPrivateDataSettings
+ );
+ setEventListener(
+ "passwordExceptions",
+ "command",
+ gPrivacyPane.showPasswordExceptions
+ );
+ setEventListener(
+ "useMasterPassword",
+ "command",
+ gPrivacyPane.updateMasterPasswordButton
+ );
+ setEventListener(
+ "changeMasterPassword",
+ "command",
+ gPrivacyPane.changeMasterPassword
+ );
+ setEventListener("showPasswords", "command", gPrivacyPane.showPasswords);
+ setEventListener(
+ "addonExceptions",
+ "command",
+ gPrivacyPane.showAddonExceptions
+ );
+ setEventListener(
+ "viewCertificatesButton",
+ "command",
+ gPrivacyPane.showCertificates
+ );
+ setEventListener(
+ "viewSecurityDevicesButton",
+ "command",
+ gPrivacyPane.showSecurityDevices
+ );
+
+ this._pane = document.getElementById("panePrivacy");
+
+ this._initPasswordGenerationUI();
+ this._initRelayIntegrationUI();
+ this._initMasterPasswordUI();
+
+ this.initListenersForExtensionControllingPasswordManager();
+
+ this._initSafeBrowsing();
+
+ setEventListener(
+ "autoplaySettingsButton",
+ "command",
+ gPrivacyPane.showAutoplayMediaExceptions
+ );
+ setEventListener(
+ "notificationSettingsButton",
+ "command",
+ gPrivacyPane.showNotificationExceptions
+ );
+ setEventListener(
+ "locationSettingsButton",
+ "command",
+ gPrivacyPane.showLocationExceptions
+ );
+ setEventListener(
+ "xrSettingsButton",
+ "command",
+ gPrivacyPane.showXRExceptions
+ );
+ setEventListener(
+ "cameraSettingsButton",
+ "command",
+ gPrivacyPane.showCameraExceptions
+ );
+ setEventListener(
+ "microphoneSettingsButton",
+ "command",
+ gPrivacyPane.showMicrophoneExceptions
+ );
+ document.getElementById("speakerSettingsRow").hidden =
+ !Services.prefs.getBoolPref("media.setsinkid.enabled", false);
+ setEventListener(
+ "speakerSettingsButton",
+ "command",
+ gPrivacyPane.showSpeakerExceptions
+ );
+ setEventListener(
+ "popupPolicyButton",
+ "command",
+ gPrivacyPane.showPopupExceptions
+ );
+ setEventListener(
+ "notificationsDoNotDisturb",
+ "command",
+ gPrivacyPane.toggleDoNotDisturbNotifications
+ );
+
+ setSyncFromPrefListener("contentBlockingBlockCookiesCheckbox", () =>
+ this.readBlockCookies()
+ );
+ setSyncToPrefListener("contentBlockingBlockCookiesCheckbox", () =>
+ this.writeBlockCookies()
+ );
+ setSyncFromPrefListener("blockCookiesMenu", () =>
+ this.readBlockCookiesFrom()
+ );
+ setSyncToPrefListener("blockCookiesMenu", () =>
+ this.writeBlockCookiesFrom()
+ );
+
+ setSyncFromPrefListener("savePasswords", () => this.readSavePasswords());
+
+ let microControlHandler = el =>
+ this.ensurePrivacyMicroControlUncheckedWhenDisabled(el);
+ setSyncFromPrefListener("rememberHistory", microControlHandler);
+ setSyncFromPrefListener("rememberForms", microControlHandler);
+ setSyncFromPrefListener("alwaysClear", microControlHandler);
+
+ setSyncFromPrefListener("popupPolicy", () =>
+ this.updateButtons("popupPolicyButton", "dom.disable_open_during_load")
+ );
+ setSyncFromPrefListener("warnAddonInstall", () =>
+ this.readWarnAddonInstall()
+ );
+ setSyncFromPrefListener("enableOCSP", () => this.readEnableOCSP());
+ setSyncToPrefListener("enableOCSP", () => this.writeEnableOCSP());
+
+ if (AlertsServiceDND) {
+ let notificationsDoNotDisturbBox = document.getElementById(
+ "notificationsDoNotDisturbBox"
+ );
+ notificationsDoNotDisturbBox.removeAttribute("hidden");
+ let checkbox = document.getElementById("notificationsDoNotDisturb");
+ document.l10n.setAttributes(checkbox, "permissions-notification-pause");
+ if (AlertsServiceDND.manualDoNotDisturb) {
+ let notificationsDoNotDisturb = document.getElementById(
+ "notificationsDoNotDisturb"
+ );
+ notificationsDoNotDisturb.setAttribute("checked", true);
+ }
+ }
+
+ this._initAddressBar();
+
+ this.initSiteDataControls();
+ setEventListener(
+ "clearSiteDataButton",
+ "command",
+ gPrivacyPane.clearSiteData
+ );
+ setEventListener(
+ "siteDataSettings",
+ "command",
+ gPrivacyPane.showSiteDataSettings
+ );
+
+ this.initCookieBannerHandling();
+
+ this.initDataCollection();
+
+ if (AppConstants.MOZ_DATA_REPORTING) {
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ this.initSubmitCrashes();
+ }
+ this.initSubmitHealthReport();
+ setEventListener(
+ "submitHealthReportBox",
+ "command",
+ gPrivacyPane.updateSubmitHealthReport
+ );
+ setEventListener(
+ "telemetryDataDeletionLearnMore",
+ "click",
+ gPrivacyPane.showDataDeletion
+ );
+ if (AppConstants.MOZ_NORMANDY) {
+ this.initOptOutStudyCheckbox();
+ }
+ this.initAddonRecommendationsCheckbox();
+ }
+
+ let signonBundle = document.getElementById("signonBundle");
+ let pkiBundle = document.getElementById("pkiBundle");
+ appendSearchKeywords("showPasswords", [
+ signonBundle.getString("loginsDescriptionAll2"),
+ ]);
+ appendSearchKeywords("viewSecurityDevicesButton", [
+ pkiBundle.getString("enable_fips"),
+ ]);
+
+ if (!PrivateBrowsingUtils.enabled) {
+ document.getElementById("privateBrowsingAutoStart").hidden = true;
+ document.querySelector("menuitem[value='dontremember']").hidden = true;
+ }
+
+ /* init HTTPS-Only mode */
+ this.initHttpsOnly();
+
+ this.initDoH();
+
+ // Notify observers that the UI is now ready
+ Services.obs.notifyObservers(window, "privacy-pane-loaded");
+ },
+
+ initSiteDataControls() {
+ Services.obs.addObserver(this, "sitedatamanager:sites-updated");
+ Services.obs.addObserver(this, "sitedatamanager:updating-sites");
+ let unload = () => {
+ window.removeEventListener("unload", unload);
+ Services.obs.removeObserver(this, "sitedatamanager:sites-updated");
+ Services.obs.removeObserver(this, "sitedatamanager:updating-sites");
+ };
+ window.addEventListener("unload", unload);
+ SiteDataManager.updateSites();
+ },
+
+ // CONTENT BLOCKING
+
+ /**
+ * Initializes the content blocking section.
+ */
+ initContentBlocking() {
+ setEventListener(
+ "contentBlockingTrackingProtectionCheckbox",
+ "command",
+ this.trackingProtectionWritePrefs
+ );
+ setEventListener(
+ "contentBlockingTrackingProtectionCheckbox",
+ "command",
+ this._updateTrackingProtectionUI
+ );
+ setEventListener(
+ "contentBlockingCryptominersCheckbox",
+ "command",
+ this.updateCryptominingLists
+ );
+ setEventListener(
+ "contentBlockingFingerprintersCheckbox",
+ "command",
+ this.updateFingerprintingLists
+ );
+ setEventListener(
+ "trackingProtectionMenu",
+ "command",
+ this.trackingProtectionWritePrefs
+ );
+ setEventListener("standardArrow", "command", this.toggleExpansion);
+ setEventListener("strictArrow", "command", this.toggleExpansion);
+ setEventListener("customArrow", "command", this.toggleExpansion);
+
+ Preferences.get("network.cookie.cookieBehavior").on(
+ "change",
+ gPrivacyPane.readBlockCookies.bind(gPrivacyPane)
+ );
+ Preferences.get("browser.contentblocking.category").on(
+ "change",
+ gPrivacyPane.highlightCBCategory
+ );
+
+ // If any relevant content blocking pref changes, show a warning that the changes will
+ // not be implemented until they refresh their tabs.
+ for (let pref of CONTENT_BLOCKING_PREFS) {
+ Preferences.get(pref).on("change", gPrivacyPane.maybeNotifyUserToReload);
+ // If the value changes, run populateCategoryContents, since that change might have been
+ // triggered by a default value changing in the standard category.
+ Preferences.get(pref).on("change", gPrivacyPane.populateCategoryContents);
+ }
+ Preferences.get("urlclassifier.trackingTable").on(
+ "change",
+ gPrivacyPane.maybeNotifyUserToReload
+ );
+ for (let button of document.querySelectorAll(".reload-tabs-button")) {
+ button.addEventListener("command", gPrivacyPane.reloadAllOtherTabs);
+ }
+
+ let cryptoMinersOption = document.getElementById(
+ "contentBlockingCryptominersOption"
+ );
+ let fingerprintersOption = document.getElementById(
+ "contentBlockingFingerprintersOption"
+ );
+ let trackingAndIsolateOption = document.querySelector(
+ "#blockCookiesMenu menuitem[value='trackers-plus-isolate']"
+ );
+ cryptoMinersOption.hidden = !Services.prefs.getBoolPref(
+ "browser.contentblocking.cryptomining.preferences.ui.enabled"
+ );
+ fingerprintersOption.hidden = !Services.prefs.getBoolPref(
+ "browser.contentblocking.fingerprinting.preferences.ui.enabled"
+ );
+ let updateTrackingAndIsolateOption = () => {
+ trackingAndIsolateOption.hidden =
+ !Services.prefs.getBoolPref(
+ "browser.contentblocking.reject-and-isolate-cookies.preferences.ui.enabled",
+ false
+ ) || gIsFirstPartyIsolated;
+ };
+ Preferences.get("privacy.firstparty.isolate").on(
+ "change",
+ updateTrackingAndIsolateOption
+ );
+ updateTrackingAndIsolateOption();
+
+ Preferences.get("browser.contentblocking.features.strict").on(
+ "change",
+ this.populateCategoryContents
+ );
+ this.populateCategoryContents();
+ this.highlightCBCategory();
+ this.readBlockCookies();
+
+ // Toggles the text "Cross-site and social media trackers" based on the
+ // social tracking pref. If the pref is false, the text reads
+ // "Cross-site trackers".
+ const STP_COOKIES_PREF = "privacy.socialtracking.block_cookies.enabled";
+ if (Services.prefs.getBoolPref(STP_COOKIES_PREF)) {
+ let contentBlockOptionSocialMedia = document.getElementById(
+ "blockCookiesSocialMedia"
+ );
+
+ document.l10n.setAttributes(
+ contentBlockOptionSocialMedia,
+ "sitedata-option-block-cross-site-tracking-cookies"
+ );
+ }
+
+ setUpContentBlockingWarnings();
+
+ initTCPStandardSection();
+ },
+
+ populateCategoryContents() {
+ for (let type of ["strict", "standard"]) {
+ let rulesArray = [];
+ let selector;
+ if (type == "strict") {
+ selector = "#contentBlockingOptionStrict";
+ rulesArray = Services.prefs
+ .getStringPref("browser.contentblocking.features.strict")
+ .split(",");
+ if (gIsFirstPartyIsolated) {
+ let idx = rulesArray.indexOf("cookieBehavior5");
+ if (idx != -1) {
+ rulesArray[idx] = "cookieBehavior4";
+ }
+ }
+ } else {
+ selector = "#contentBlockingOptionStandard";
+ // In standard show/hide UI items based on the default values of the relevant prefs.
+ let defaults = Services.prefs.getDefaultBranch("");
+
+ let cookieBehavior = defaults.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+ switch (cookieBehavior) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ rulesArray.push("cookieBehavior0");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ rulesArray.push("cookieBehavior1");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ rulesArray.push("cookieBehavior2");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ rulesArray.push("cookieBehavior3");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ rulesArray.push("cookieBehavior4");
+ break;
+ case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ rulesArray.push(
+ gIsFirstPartyIsolated ? "cookieBehavior4" : "cookieBehavior5"
+ );
+ break;
+ }
+ let cookieBehaviorPBM = defaults.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ );
+ switch (cookieBehaviorPBM) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ rulesArray.push("cookieBehaviorPBM0");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ rulesArray.push("cookieBehaviorPBM1");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ rulesArray.push("cookieBehaviorPBM2");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ rulesArray.push("cookieBehaviorPBM3");
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ rulesArray.push("cookieBehaviorPBM4");
+ break;
+ case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ rulesArray.push(
+ gIsFirstPartyIsolated
+ ? "cookieBehaviorPBM4"
+ : "cookieBehaviorPBM5"
+ );
+ break;
+ }
+ rulesArray.push(
+ defaults.getBoolPref(
+ "privacy.trackingprotection.cryptomining.enabled"
+ )
+ ? "cm"
+ : "-cm"
+ );
+ rulesArray.push(
+ defaults.getBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled"
+ )
+ ? "fp"
+ : "-fp"
+ );
+ rulesArray.push(
+ Services.prefs.getBoolPref(
+ "privacy.socialtracking.block_cookies.enabled"
+ )
+ ? "stp"
+ : "-stp"
+ );
+ rulesArray.push(
+ defaults.getBoolPref("privacy.trackingprotection.enabled")
+ ? "tp"
+ : "-tp"
+ );
+ rulesArray.push(
+ defaults.getBoolPref("privacy.trackingprotection.pbmode.enabled")
+ ? "tpPrivate"
+ : "-tpPrivate"
+ );
+ }
+
+ // Hide all cookie options first, until we learn which one should be showing.
+ document.querySelector(selector + " .all-cookies-option").hidden = true;
+ document.querySelector(
+ selector + " .unvisited-cookies-option"
+ ).hidden = true;
+ document.querySelector(
+ selector + " .cross-site-cookies-option"
+ ).hidden = true;
+ document.querySelector(
+ selector + " .third-party-tracking-cookies-option"
+ ).hidden = true;
+ document.querySelector(
+ selector + " .all-third-party-cookies-private-windows-option"
+ ).hidden = true;
+ document.querySelector(
+ selector + " .all-third-party-cookies-option"
+ ).hidden = true;
+ document.querySelector(selector + " .social-media-option").hidden = true;
+
+ for (let item of rulesArray) {
+ // Note "cookieBehavior0", will result in no UI changes, so is not listed here.
+ switch (item) {
+ case "tp":
+ document.querySelector(
+ selector + " .trackers-option"
+ ).hidden = false;
+ break;
+ case "-tp":
+ document.querySelector(
+ selector + " .trackers-option"
+ ).hidden = true;
+ break;
+ case "tpPrivate":
+ document.querySelector(
+ selector + " .pb-trackers-option"
+ ).hidden = false;
+ break;
+ case "-tpPrivate":
+ document.querySelector(
+ selector + " .pb-trackers-option"
+ ).hidden = true;
+ break;
+ case "fp":
+ document.querySelector(
+ selector + " .fingerprinters-option"
+ ).hidden = false;
+ break;
+ case "-fp":
+ document.querySelector(
+ selector + " .fingerprinters-option"
+ ).hidden = true;
+ break;
+ case "cm":
+ document.querySelector(
+ selector + " .cryptominers-option"
+ ).hidden = false;
+ break;
+ case "-cm":
+ document.querySelector(
+ selector + " .cryptominers-option"
+ ).hidden = true;
+ break;
+ case "stp":
+ // Store social tracking cookies pref
+ const STP_COOKIES_PREF =
+ "privacy.socialtracking.block_cookies.enabled";
+
+ if (Services.prefs.getBoolPref(STP_COOKIES_PREF)) {
+ document.querySelector(
+ selector + " .social-media-option"
+ ).hidden = false;
+ }
+ break;
+ case "-stp":
+ // Store social tracking cookies pref
+ document.querySelector(
+ selector + " .social-media-option"
+ ).hidden = true;
+ break;
+ case "cookieBehavior1":
+ document.querySelector(
+ selector + " .all-third-party-cookies-option"
+ ).hidden = false;
+ break;
+ case "cookieBehavior2":
+ document.querySelector(
+ selector + " .all-cookies-option"
+ ).hidden = false;
+ break;
+ case "cookieBehavior3":
+ document.querySelector(
+ selector + " .unvisited-cookies-option"
+ ).hidden = false;
+ break;
+ case "cookieBehavior4":
+ document.querySelector(
+ selector + " .third-party-tracking-cookies-option"
+ ).hidden = false;
+ break;
+ case "cookieBehavior5":
+ document.querySelector(
+ selector + " .cross-site-cookies-option"
+ ).hidden = false;
+ break;
+ case "cookieBehaviorPBM5":
+ // We only need to show the cookie option for private windows if the
+ // cookieBehaviors are different between regular windows and private
+ // windows.
+ if (!rulesArray.includes("cookieBehavior5")) {
+ document.querySelector(
+ selector + " .all-third-party-cookies-private-windows-option"
+ ).hidden = false;
+ }
+ break;
+ }
+ }
+ // Hide the "tracking protection in private browsing" list item
+ // if the "tracking protection enabled in all windows" list item is showing.
+ if (!document.querySelector(selector + " .trackers-option").hidden) {
+ document.querySelector(selector + " .pb-trackers-option").hidden = true;
+ }
+ }
+ },
+
+ highlightCBCategory() {
+ let value = Preferences.get("browser.contentblocking.category").value;
+ let standardEl = document.getElementById("contentBlockingOptionStandard");
+ let strictEl = document.getElementById("contentBlockingOptionStrict");
+ let customEl = document.getElementById("contentBlockingOptionCustom");
+ standardEl.classList.remove("selected");
+ strictEl.classList.remove("selected");
+ customEl.classList.remove("selected");
+
+ switch (value) {
+ case "strict":
+ strictEl.classList.add("selected");
+ break;
+ case "custom":
+ customEl.classList.add("selected");
+ break;
+ case "standard":
+ /* fall through */
+ default:
+ standardEl.classList.add("selected");
+ break;
+ }
+ },
+
+ updateCryptominingLists() {
+ let listPrefs = [
+ "urlclassifier.features.cryptomining.blacklistTables",
+ "urlclassifier.features.cryptomining.whitelistTables",
+ ];
+
+ let listValue = listPrefs
+ .map(l => Services.prefs.getStringPref(l))
+ .join(",");
+ listManager.forceUpdates(listValue);
+ },
+
+ updateFingerprintingLists() {
+ let listPrefs = [
+ "urlclassifier.features.fingerprinting.blacklistTables",
+ "urlclassifier.features.fingerprinting.whitelistTables",
+ ];
+
+ let listValue = listPrefs
+ .map(l => Services.prefs.getStringPref(l))
+ .join(",");
+ listManager.forceUpdates(listValue);
+ },
+
+ // TRACKING PROTECTION MODE
+
+ /**
+ * Selects the right item of the Tracking Protection menulist and checkbox.
+ */
+ trackingProtectionReadPrefs() {
+ let enabledPref = Preferences.get("privacy.trackingprotection.enabled");
+ let pbmPref = Preferences.get("privacy.trackingprotection.pbmode.enabled");
+ let tpMenu = document.getElementById("trackingProtectionMenu");
+ let tpCheckbox = document.getElementById(
+ "contentBlockingTrackingProtectionCheckbox"
+ );
+
+ this._updateTrackingProtectionUI();
+
+ // Global enable takes precedence over enabled in Private Browsing.
+ if (enabledPref.value) {
+ tpMenu.value = "always";
+ tpCheckbox.checked = true;
+ } else if (pbmPref.value) {
+ tpMenu.value = "private";
+ tpCheckbox.checked = true;
+ } else {
+ tpMenu.value = "never";
+ tpCheckbox.checked = false;
+ }
+ },
+
+ /**
+ * Selects the right items of the new Cookies & Site Data UI.
+ */
+ networkCookieBehaviorReadPrefs() {
+ let behavior = Services.cookies.getCookieBehavior(false);
+ let blockCookiesMenu = document.getElementById("blockCookiesMenu");
+ let deleteOnCloseCheckbox = document.getElementById("deleteOnClose");
+ let deleteOnCloseNote = document.getElementById("deleteOnCloseNote");
+ let blockCookies = behavior != Ci.nsICookieService.BEHAVIOR_ACCEPT;
+ let cookieBehaviorLocked = Services.prefs.prefIsLocked(
+ "network.cookie.cookieBehavior"
+ );
+ let blockCookiesControlsDisabled = !blockCookies || cookieBehaviorLocked;
+ blockCookiesMenu.disabled = blockCookiesControlsDisabled;
+
+ let completelyBlockCookies =
+ behavior == Ci.nsICookieService.BEHAVIOR_REJECT;
+ let privateBrowsing = Preferences.get(
+ "browser.privatebrowsing.autostart"
+ ).value;
+ deleteOnCloseCheckbox.disabled = privateBrowsing || completelyBlockCookies;
+ deleteOnCloseNote.hidden = !privateBrowsing;
+
+ switch (behavior) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ blockCookiesMenu.value = "all-third-parties";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ blockCookiesMenu.value = "always";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ blockCookiesMenu.value = "unvisited";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ blockCookiesMenu.value = "trackers";
+ break;
+ case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ blockCookiesMenu.value = "trackers-plus-isolate";
+ break;
+ }
+ },
+
+ /**
+ * Sets the pref values based on the selected item of the radiogroup.
+ */
+ trackingProtectionWritePrefs() {
+ let enabledPref = Preferences.get("privacy.trackingprotection.enabled");
+ let pbmPref = Preferences.get("privacy.trackingprotection.pbmode.enabled");
+ let stpPref = Preferences.get(
+ "privacy.trackingprotection.socialtracking.enabled"
+ );
+ let stpCookiePref = Preferences.get(
+ "privacy.socialtracking.block_cookies.enabled"
+ );
+ // Currently, we don't expose the email tracking protection setting on our
+ // privacy UI. Instead, we use the existing tracking protection checkbox to
+ // control the email tracking protection.
+ let emailTPPref = Preferences.get(
+ "privacy.trackingprotection.emailtracking.enabled"
+ );
+ let emailTPPBMPref = Preferences.get(
+ "privacy.trackingprotection.emailtracking.pbmode.enabled"
+ );
+ let tpMenu = document.getElementById("trackingProtectionMenu");
+ let tpCheckbox = document.getElementById(
+ "contentBlockingTrackingProtectionCheckbox"
+ );
+
+ let value;
+ if (tpCheckbox.checked) {
+ if (tpMenu.value == "never") {
+ tpMenu.value = "private";
+ }
+ value = tpMenu.value;
+ } else {
+ tpMenu.value = "never";
+ value = "never";
+ }
+
+ switch (value) {
+ case "always":
+ enabledPref.value = true;
+ pbmPref.value = true;
+ emailTPPref.value = true;
+ emailTPPBMPref.value = true;
+ if (stpCookiePref.value) {
+ stpPref.value = true;
+ }
+ break;
+ case "private":
+ enabledPref.value = false;
+ pbmPref.value = true;
+ emailTPPref.value = false;
+ emailTPPBMPref.value = true;
+ if (stpCookiePref.value) {
+ stpPref.value = false;
+ }
+ break;
+ case "never":
+ enabledPref.value = false;
+ pbmPref.value = false;
+ emailTPPref.value = false;
+ emailTPPBMPref.value = false;
+ if (stpCookiePref.value) {
+ stpPref.value = false;
+ }
+ break;
+ }
+ },
+
+ toggleExpansion(e) {
+ let carat = e.target;
+ carat.classList.toggle("up");
+ carat.closest(".privacy-detailedoption").classList.toggle("expanded");
+ carat.setAttribute(
+ "aria-expanded",
+ carat.getAttribute("aria-expanded") === "false"
+ );
+ },
+
+ // HISTORY MODE
+
+ /**
+ * The list of preferences which affect the initial history mode settings.
+ * If the auto start private browsing mode pref is active, the initial
+ * history mode would be set to "Don't remember anything".
+ * If ALL of these preferences are set to the values that correspond
+ * to keeping some part of history, and the auto-start
+ * private browsing mode is not active, the initial history mode would be
+ * set to "Remember everything".
+ * Otherwise, the initial history mode would be set to "Custom".
+ *
+ * Extensions adding their own preferences can set values here if needed.
+ */
+ prefsForKeepingHistory: {
+ "places.history.enabled": true, // History is enabled
+ "browser.formfill.enable": true, // Form information is saved
+ "privacy.sanitize.sanitizeOnShutdown": false, // Private date is NOT cleared on shutdown
+ },
+
+ /**
+ * The list of control IDs which are dependent on the auto-start private
+ * browsing setting, such that in "Custom" mode they would be disabled if
+ * the auto-start private browsing checkbox is checked, and enabled otherwise.
+ *
+ * Extensions adding their own controls can append their IDs to this array if needed.
+ */
+ dependentControls: [
+ "rememberHistory",
+ "rememberForms",
+ "alwaysClear",
+ "clearDataSettings",
+ ],
+
+ /**
+ * Check whether preferences values are set to keep history
+ *
+ * @param aPrefs an array of pref names to check for
+ * @returns boolean true if all of the prefs are set to keep history,
+ * false otherwise
+ */
+ _checkHistoryValues(aPrefs) {
+ for (let pref of Object.keys(aPrefs)) {
+ if (Preferences.get(pref).value != aPrefs[pref]) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Initialize the history mode menulist based on the privacy preferences
+ */
+ initializeHistoryMode() {
+ let mode;
+ let getVal = aPref => Preferences.get(aPref).value;
+
+ if (getVal("privacy.history.custom")) {
+ mode = "custom";
+ } else if (this._checkHistoryValues(this.prefsForKeepingHistory)) {
+ if (getVal("browser.privatebrowsing.autostart")) {
+ mode = "dontremember";
+ } else {
+ mode = "remember";
+ }
+ } else {
+ mode = "custom";
+ }
+
+ document.getElementById("historyMode").value = mode;
+ },
+
+ /**
+ * Update the selected pane based on the history mode menulist
+ */
+ updateHistoryModePane() {
+ let selectedIndex = -1;
+ switch (document.getElementById("historyMode").value) {
+ case "remember":
+ selectedIndex = 0;
+ break;
+ case "dontremember":
+ selectedIndex = 1;
+ break;
+ case "custom":
+ selectedIndex = 2;
+ break;
+ }
+ document.getElementById("historyPane").selectedIndex = selectedIndex;
+ Preferences.get("privacy.history.custom").value = selectedIndex == 2;
+ },
+
+ /**
+ * Update the private browsing auto-start pref and the history mode
+ * micro-management prefs based on the history mode menulist
+ */
+ updateHistoryModePrefs() {
+ let pref = Preferences.get("browser.privatebrowsing.autostart");
+ switch (document.getElementById("historyMode").value) {
+ case "remember":
+ if (pref.value) {
+ pref.value = false;
+ }
+
+ // select the remember history option if needed
+ Preferences.get("places.history.enabled").value = true;
+
+ // select the remember forms history option
+ Preferences.get("browser.formfill.enable").value = true;
+
+ // select the clear on close option
+ Preferences.get("privacy.sanitize.sanitizeOnShutdown").value = false;
+ break;
+ case "dontremember":
+ if (!pref.value) {
+ pref.value = true;
+ }
+ break;
+ }
+ },
+
+ /**
+ * Update the privacy micro-management controls based on the
+ * value of the private browsing auto-start preference.
+ */
+ updatePrivacyMicroControls() {
+ let clearDataSettings = document.getElementById("clearDataSettings");
+
+ if (document.getElementById("historyMode").value == "custom") {
+ let disabled = Preferences.get("browser.privatebrowsing.autostart").value;
+ this.dependentControls.forEach(aElement => {
+ let control = document.getElementById(aElement);
+ let preferenceId = control.getAttribute("preference");
+ if (!preferenceId) {
+ let dependentControlId = control.getAttribute("control");
+ if (dependentControlId) {
+ let dependentControl = document.getElementById(dependentControlId);
+ preferenceId = dependentControl.getAttribute("preference");
+ }
+ }
+
+ let preference = preferenceId ? Preferences.get(preferenceId) : {};
+ control.disabled = disabled || preference.locked;
+ if (control != clearDataSettings) {
+ this.ensurePrivacyMicroControlUncheckedWhenDisabled(control);
+ }
+ });
+
+ clearDataSettings.removeAttribute("hidden");
+
+ if (!disabled) {
+ // adjust the Settings button for sanitizeOnShutdown
+ this._updateSanitizeSettingsButton();
+ }
+ } else {
+ clearDataSettings.hidden = true;
+ }
+ },
+
+ ensurePrivacyMicroControlUncheckedWhenDisabled(el) {
+ if (Preferences.get("browser.privatebrowsing.autostart").value) {
+ // Set checked to false when called from updatePrivacyMicroControls
+ el.checked = false;
+ // return false for the onsyncfrompreference case:
+ return false;
+ }
+ return undefined; // tell preferencesBindings to assign the 'right' value.
+ },
+
+ // CLEAR PRIVATE DATA
+
+ /*
+ * Preferences:
+ *
+ * privacy.sanitize.sanitizeOnShutdown
+ * - true if the user's private data is cleared on startup according to the
+ * Clear Private Data settings, false otherwise
+ */
+
+ /**
+ * Displays the Clear Private Data settings dialog.
+ */
+ showClearPrivateDataSettings() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sanitize.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ /**
+ * Displays a dialog from which individual parts of private data may be
+ * cleared.
+ */
+ clearPrivateDataNow(aClearEverything) {
+ var ts = Preferences.get("privacy.sanitize.timeSpan");
+ var timeSpanOrig = ts.value;
+
+ if (aClearEverything) {
+ ts.value = 0;
+ }
+
+ gSubDialog.open("chrome://browser/content/sanitize.xhtml", {
+ features: "resizable=no",
+ closingCallback: () => {
+ // reset the timeSpan pref
+ if (aClearEverything) {
+ ts.value = timeSpanOrig;
+ }
+
+ Services.obs.notifyObservers(null, "clear-private-data");
+ },
+ });
+ },
+
+ /*
+ * On loading the page, assigns the state to the deleteOnClose checkbox that fits the pref selection
+ */
+ initDeleteOnCloseBox() {
+ let deleteOnCloseBox = document.getElementById("deleteOnClose");
+ deleteOnCloseBox.checked =
+ (Preferences.get("privacy.sanitize.sanitizeOnShutdown").value &&
+ Preferences.get("privacy.clearOnShutdown.cookies").value &&
+ Preferences.get("privacy.clearOnShutdown.cache").value &&
+ Preferences.get("privacy.clearOnShutdown.offlineApps").value) ||
+ Preferences.get("browser.privatebrowsing.autostart").value;
+ },
+
+ /*
+ * Keeps the state of the deleteOnClose checkbox in sync with the pref selection
+ */
+ syncSanitizationPrefsWithDeleteOnClose() {
+ let deleteOnCloseBox = document.getElementById("deleteOnClose");
+ let historyMode = Preferences.get("privacy.history.custom");
+ let sanitizeOnShutdownPref = Preferences.get(
+ "privacy.sanitize.sanitizeOnShutdown"
+ );
+ // ClearOnClose cleaning categories
+ let cookiePref = Preferences.get("privacy.clearOnShutdown.cookies");
+ let cachePref = Preferences.get("privacy.clearOnShutdown.cache");
+ let offlineAppsPref = Preferences.get(
+ "privacy.clearOnShutdown.offlineApps"
+ );
+
+ // Sync the cleaning prefs with the deleteOnClose box
+ deleteOnCloseBox.addEventListener("command", () => {
+ let { checked } = deleteOnCloseBox;
+ cookiePref.value = checked;
+ cachePref.value = checked;
+ offlineAppsPref.value = checked;
+ // Forget the current pref selection if sanitizeOnShutdown is disabled,
+ // to not over clear when it gets enabled by the sync mechanism
+ if (!sanitizeOnShutdownPref.value) {
+ this._resetCleaningPrefs();
+ }
+ // If no other cleaning category is selected, sanitizeOnShutdown gets synced with deleteOnClose
+ sanitizeOnShutdownPref.value =
+ this._isCustomCleaningPrefPresent() || checked;
+
+ // Update the view of the history settings
+ if (checked && !historyMode.value) {
+ historyMode.value = "custom";
+ this.initializeHistoryMode();
+ this.updateHistoryModePane();
+ this.updatePrivacyMicroControls();
+ }
+ });
+
+ cookiePref.on("change", this._onSanitizePrefChangeSyncClearOnClose);
+ cachePref.on("change", this._onSanitizePrefChangeSyncClearOnClose);
+ offlineAppsPref.on("change", this._onSanitizePrefChangeSyncClearOnClose);
+ sanitizeOnShutdownPref.on(
+ "change",
+ this._onSanitizePrefChangeSyncClearOnClose
+ );
+ },
+
+ /*
+ * Sync the deleteOnClose box to its cleaning prefs
+ */
+ _onSanitizePrefChangeSyncClearOnClose() {
+ let deleteOnCloseBox = document.getElementById("deleteOnClose");
+ deleteOnCloseBox.checked =
+ Preferences.get("privacy.clearOnShutdown.cookies").value &&
+ Preferences.get("privacy.clearOnShutdown.cache").value &&
+ Preferences.get("privacy.clearOnShutdown.offlineApps").value &&
+ Preferences.get("privacy.sanitize.sanitizeOnShutdown").value;
+ },
+
+ /*
+ * Unsets cleaning prefs that do not belong to DeleteOnClose
+ */
+ _resetCleaningPrefs() {
+ SANITIZE_ON_SHUTDOWN_PREFS_ONLY.forEach(
+ pref => (Preferences.get(pref).value = false)
+ );
+ },
+
+ /*
+ Checks if the user set cleaning prefs that do not belong to DeleteOnClose
+ */
+ _isCustomCleaningPrefPresent() {
+ return SANITIZE_ON_SHUTDOWN_PREFS_ONLY.some(
+ pref => Preferences.get(pref).value
+ );
+ },
+
+ /**
+ * Enables or disables the "Settings..." button depending
+ * on the privacy.sanitize.sanitizeOnShutdown preference value
+ */
+ _updateSanitizeSettingsButton() {
+ var settingsButton = document.getElementById("clearDataSettings");
+ var sanitizeOnShutdownPref = Preferences.get(
+ "privacy.sanitize.sanitizeOnShutdown"
+ );
+
+ settingsButton.disabled = !sanitizeOnShutdownPref.value;
+ },
+
+ toggleDoNotDisturbNotifications(event) {
+ AlertsServiceDND.manualDoNotDisturb = event.target.checked;
+ },
+
+ // PRIVATE BROWSING
+
+ /**
+ * Initialize the starting state for the auto-start private browsing mode pref reverter.
+ */
+ initAutoStartPrivateBrowsingReverter() {
+ // We determine the mode in initializeHistoryMode, which is guaranteed to have been
+ // called before now, so this is up-to-date.
+ let mode = document.getElementById("historyMode");
+ this._lastMode = mode.selectedIndex;
+ // The value of the autostart pref, on the other hand, is gotten from Preferences,
+ // which updates the DOM asynchronously, so we can't rely on the DOM. Get it directly
+ // from the prefs.
+ this._lastCheckState = Preferences.get(
+ "browser.privatebrowsing.autostart"
+ ).value;
+ },
+
+ _lastMode: null,
+ _lastCheckState: null,
+ async updateAutostart() {
+ let mode = document.getElementById("historyMode");
+ let autoStart = document.getElementById("privateBrowsingAutoStart");
+ let pref = Preferences.get("browser.privatebrowsing.autostart");
+ if (
+ (mode.value == "custom" && this._lastCheckState == autoStart.checked) ||
+ (mode.value == "remember" && !this._lastCheckState) ||
+ (mode.value == "dontremember" && this._lastCheckState)
+ ) {
+ // These are all no-op changes, so we don't need to prompt.
+ this._lastMode = mode.selectedIndex;
+ this._lastCheckState = autoStart.hasAttribute("checked");
+ return;
+ }
+
+ if (!this._shouldPromptForRestart) {
+ // We're performing a revert. Just let it happen.
+ return;
+ }
+
+ let buttonIndex = await confirmRestartPrompt(
+ autoStart.checked,
+ 1,
+ true,
+ false
+ );
+ if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
+ pref.value = autoStart.hasAttribute("checked");
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ return;
+ }
+
+ this._shouldPromptForRestart = false;
+
+ if (this._lastCheckState) {
+ autoStart.checked = "checked";
+ } else {
+ autoStart.removeAttribute("checked");
+ }
+ pref.value = autoStart.hasAttribute("checked");
+ mode.selectedIndex = this._lastMode;
+ mode.doCommand();
+
+ this._shouldPromptForRestart = true;
+ },
+
+ /**
+ * Displays fine-grained, per-site preferences for tracking protection.
+ */
+ showTrackingProtectionExceptions() {
+ let params = {
+ permissionType: "trackingprotection",
+ disableETPVisible: true,
+ prefilledHost: "",
+ hideStatusColumn: true,
+ };
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml",
+ undefined,
+ params
+ );
+ },
+
+ /**
+ * Displays the available block lists for tracking protection.
+ */
+ showBlockLists() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/blocklists.xhtml"
+ );
+ },
+
+ // COOKIES AND SITE DATA
+
+ /*
+ * Preferences:
+ *
+ * network.cookie.cookieBehavior
+ * - determines how the browser should handle cookies:
+ * 0 means enable all cookies
+ * 1 means reject all third party cookies
+ * 2 means disable all cookies
+ * 3 means reject third party cookies unless at least one is already set for the eTLD
+ * 4 means reject all trackers
+ * 5 means reject all trackers and partition third-party cookies
+ * see netwerk/cookie/src/CookieService.cpp for details
+ */
+
+ /**
+ * Reads the network.cookie.cookieBehavior preference value and
+ * enables/disables the "blockCookiesMenu" menulist accordingly.
+ */
+ readBlockCookies() {
+ let bcControl = document.getElementById("blockCookiesMenu");
+ bcControl.disabled =
+ Services.cookies.getCookieBehavior(false) ==
+ Ci.nsICookieService.BEHAVIOR_ACCEPT;
+ },
+
+ /**
+ * Updates the "accept third party cookies" menu based on whether the
+ * "contentBlockingBlockCookiesCheckbox" checkbox is checked.
+ */
+ writeBlockCookies() {
+ let block = document.getElementById("contentBlockingBlockCookiesCheckbox");
+ let blockCookiesMenu = document.getElementById("blockCookiesMenu");
+
+ if (block.checked) {
+ // Automatically select 'third-party trackers' as the default.
+ blockCookiesMenu.selectedIndex = 0;
+ return this.writeBlockCookiesFrom();
+ }
+ return Ci.nsICookieService.BEHAVIOR_ACCEPT;
+ },
+
+ readBlockCookiesFrom() {
+ switch (Services.cookies.getCookieBehavior(false)) {
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ return "all-third-parties";
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ return "always";
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ return "unvisited";
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ return "trackers";
+ case BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ return "trackers-plus-isolate";
+ default:
+ return undefined;
+ }
+ },
+
+ writeBlockCookiesFrom() {
+ let block = document.getElementById("blockCookiesMenu").selectedItem;
+ switch (block.value) {
+ case "trackers":
+ return Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
+ case "unvisited":
+ return Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN;
+ case "always":
+ return Ci.nsICookieService.BEHAVIOR_REJECT;
+ case "all-third-parties":
+ return Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN;
+ case "trackers-plus-isolate":
+ return Ci.nsICookieService
+ .BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+ default:
+ return undefined;
+ }
+ },
+
+ /**
+ * Discard the browsers of all tabs in all windows. Pinned tabs, as
+ * well as tabs for which discarding doesn't succeed (e.g. selected
+ * tabs, tabs with beforeunload listeners), are reloaded.
+ */
+ reloadAllOtherTabs() {
+ let ourTab = BrowserWindowTracker.getTopWindow().gBrowser.selectedTab;
+ BrowserWindowTracker.orderedWindows.forEach(win => {
+ let otherGBrowser = win.gBrowser;
+ for (let tab of otherGBrowser.tabs) {
+ if (tab == ourTab) {
+ // Don't reload our preferences tab.
+ continue;
+ }
+
+ if (tab.pinned || tab.selected) {
+ otherGBrowser.reloadTab(tab);
+ } else {
+ otherGBrowser.discardBrowser(tab);
+ }
+ }
+ });
+
+ for (let notification of document.querySelectorAll(".reload-tabs")) {
+ notification.hidden = true;
+ }
+ },
+
+ /**
+ * If there are more tabs than just the preferences tab, show a warning to the user that
+ * they need to reload their tabs to apply the setting.
+ */
+ maybeNotifyUserToReload() {
+ let shouldShow = false;
+ if (window.BrowserWindowTracker.orderedWindows.length > 1) {
+ shouldShow = true;
+ } else {
+ let tabbrowser = window.BrowserWindowTracker.getTopWindow().gBrowser;
+ if (tabbrowser.tabs.length > 1) {
+ shouldShow = true;
+ }
+ }
+ if (shouldShow) {
+ for (let notification of document.querySelectorAll(".reload-tabs")) {
+ notification.hidden = false;
+ }
+ }
+ },
+
+ /**
+ * Displays fine-grained, per-site preferences for cookies.
+ */
+ showCookieExceptions() {
+ var params = {
+ blockVisible: true,
+ sessionVisible: true,
+ allowVisible: true,
+ prefilledHost: "",
+ permissionType: "cookie",
+ };
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml",
+ undefined,
+ params
+ );
+ },
+
+ /**
+ * Displays per-site preferences for HTTPS-Only Mode exceptions.
+ */
+ showHttpsOnlyModeExceptions() {
+ var params = {
+ blockVisible: false,
+ sessionVisible: true,
+ allowVisible: false,
+ prefilledHost: "",
+ permissionType: "https-only-load-insecure",
+ };
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml",
+ undefined,
+ params
+ );
+ },
+
+ showDoHExceptions() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/dohExceptions.xhtml",
+ undefined
+ );
+ },
+
+ showSiteDataSettings() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml"
+ );
+ },
+
+ toggleSiteData(shouldShow) {
+ let clearButton = document.getElementById("clearSiteDataButton");
+ let settingsButton = document.getElementById("siteDataSettings");
+ clearButton.disabled = !shouldShow;
+ settingsButton.disabled = !shouldShow;
+ },
+
+ showSiteDataLoading() {
+ let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize");
+ document.l10n.setAttributes(
+ totalSiteDataSizeLabel,
+ "sitedata-total-size-calculating"
+ );
+ },
+
+ updateTotalDataSizeLabel(siteDataUsage) {
+ SiteDataManager.getCacheSize().then(function (cacheUsage) {
+ let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize");
+ let totalUsage = siteDataUsage + cacheUsage;
+ let [value, unit] = DownloadUtils.convertByteUnits(totalUsage);
+ document.l10n.setAttributes(
+ totalSiteDataSizeLabel,
+ "sitedata-total-size",
+ {
+ value,
+ unit,
+ }
+ );
+ });
+ },
+
+ clearSiteData() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml"
+ );
+ },
+
+ /**
+ * Initializes the cookie banner handling subgroup on the privacy pane.
+ *
+ * This UI is shown if the "cookiebanners.ui.desktop.enabled" pref is true.
+ *
+ * The cookie banner handling checkbox reflects the cookie banner feature
+ * state. It is enabled when the service enabled via the
+ * cookiebanners.service.mode pref. If detection-only mode is enabled the
+ * checkbox is unchecked, since in this mode no banners are handled. It is
+ * only used for detection for banners which means we may prompt the user to
+ * enable the feature via other UI surfaces such as the onboarding doorhanger.
+ *
+ * If the user checks the checkbox, the pref value is set to
+ * nsICookieBannerService.MODE_REJECT_OR_ACCEPT.
+ *
+ * If the user unchecks the checkbox, the mode pref value is set to
+ * nsICookieBannerService.MODE_DISABLED.
+ *
+ * Advanced users can choose other int-valued modes via about:config.
+ */
+ initCookieBannerHandling() {
+ setSyncFromPrefListener("handleCookieBanners", () =>
+ this.readCookieBannerMode()
+ );
+ setSyncToPrefListener("handleCookieBanners", () =>
+ this.writeCookieBannerMode()
+ );
+
+ let preference = Preferences.get("cookiebanners.ui.desktop.enabled");
+ preference.on("change", () => this.updateCookieBannerHandlingVisibility());
+
+ this.updateCookieBannerHandlingVisibility();
+ },
+
+ /**
+ * Reads the cookiebanners.service.mode and detectOnly preference value and
+ * updates the cookie banner handling checkbox accordingly.
+ */
+ readCookieBannerMode() {
+ if (Preferences.get("cookiebanners.service.detectOnly").value) {
+ return false;
+ }
+ return (
+ Preferences.get("cookiebanners.service.mode").value !=
+ Ci.nsICookieBannerService.MODE_DISABLED
+ );
+ },
+
+ /**
+ * Translates user clicks on the cookie banner handling checkbox to the
+ * corresponding integer-valued cookie banner mode preference.
+ */
+ writeCookieBannerMode() {
+ let checkbox = document.getElementById("handleCookieBanners");
+ let mode;
+ if (checkbox.checked) {
+ mode = Ci.nsICookieBannerService.MODE_REJECT;
+
+ // Also unset the detect-only mode pref, just in case the user enabled
+ // the feature via about:preferences, not the onboarding doorhanger.
+ Services.prefs.setBoolPref("cookiebanners.service.detectOnly", false);
+ } else {
+ mode = Ci.nsICookieBannerService.MODE_DISABLED;
+ }
+
+ /**
+ * There is a second service.mode pref for private browsing,
+ * but for now we want it always be the same as service.mode
+ * more info: https://bugzilla.mozilla.org/show_bug.cgi?id=1817201
+ */
+ Services.prefs.setIntPref(
+ "cookiebanners.service.mode.privateBrowsing",
+ mode
+ );
+ return mode;
+ },
+
+ /**
+ * Shows or hides the cookie banner handling section based on the value of
+ * the "cookiebanners.ui.desktop.enabled" pref.
+ */
+ updateCookieBannerHandlingVisibility() {
+ let groupbox = document.getElementById("cookieBannerHandlingGroup");
+ let isEnabled = Preferences.get("cookiebanners.ui.desktop.enabled").value;
+
+ // Because the top-level pane showing code unsets the hidden attribute, we
+ // manually hide the section when cookie banner handling is preffed off.
+ if (isEnabled) {
+ groupbox.removeAttribute("style");
+ } else {
+ groupbox.setAttribute("style", "display: none !important");
+ }
+ },
+
+ // ADDRESS BAR
+
+ /**
+ * Initializes the address bar section.
+ */
+ _initAddressBar() {
+ // Update the Firefox Suggest section when its Nimbus config changes.
+ let onNimbus = () => this._updateFirefoxSuggestSection();
+ NimbusFeatures.urlbar.onUpdate(onNimbus);
+ window.addEventListener("unload", () => {
+ NimbusFeatures.urlbar.offUpdate(onNimbus);
+ });
+
+ // The Firefox Suggest info box potentially needs updating when any of the
+ // toggles change.
+ let infoBoxPrefs = [
+ "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ "browser.urlbar.suggest.quicksuggest.sponsored",
+ "browser.urlbar.quicksuggest.dataCollection.enabled",
+ ];
+ for (let pref of infoBoxPrefs) {
+ Preferences.get(pref).on("change", () =>
+ this._updateFirefoxSuggestInfoBox()
+ );
+ }
+
+ this._updateFirefoxSuggestSection(true);
+ this._initQuickActionsSection();
+ },
+
+ /**
+ * Updates the Firefox Suggest section (in the address bar section) depending
+ * on whether the user is enrolled in a Firefox Suggest rollout.
+ *
+ * @param {boolean} [onInit]
+ * Pass true when calling this when initializing the pane.
+ */
+ _updateFirefoxSuggestSection(onInit = false) {
+ // Show the best match checkbox container as appropriate.
+ document.getElementById("firefoxSuggestBestMatchContainer").hidden =
+ !UrlbarPrefs.get("bestMatchEnabled");
+
+ let container = document.getElementById("firefoxSuggestContainer");
+
+ if (UrlbarPrefs.get("quickSuggestEnabled")) {
+ // Update the l10n IDs of text elements.
+ let l10nIdByElementId = {
+ locationBarGroupHeader: "addressbar-header-firefox-suggest",
+ locationBarSuggestionLabel: "addressbar-suggest-firefox-suggest",
+ };
+ for (let [elementId, l10nId] of Object.entries(l10nIdByElementId)) {
+ let element = document.getElementById(elementId);
+ element.dataset.l10nIdOriginal ??= element.dataset.l10nId;
+ element.dataset.l10nId = l10nId;
+ }
+
+ // Add the extraMargin class to the engine-prefs link.
+ document
+ .getElementById("openSearchEnginePreferences")
+ .classList.add("extraMargin");
+
+ // Show the container.
+ this._updateFirefoxSuggestInfoBox();
+
+ this._updateDismissedSuggestionsStatus();
+ Preferences.get(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST).on("change", () =>
+ this._updateDismissedSuggestionsStatus()
+ );
+ Preferences.get(PREF_URLBAR_WEATHER_USER_ENABLED).on("change", () =>
+ this._updateDismissedSuggestionsStatus()
+ );
+ setEventListener("restoreDismissedSuggestions", "command", () =>
+ this.restoreDismissedSuggestions()
+ );
+
+ container.removeAttribute("hidden");
+ } else if (!onInit) {
+ // Firefox Suggest is not enabled. This is the default, so to avoid
+ // accidentally messing anything up, only modify the doc if we're being
+ // called due to a change in the rollout-enabled status (!onInit).
+ container.setAttribute("hidden", "true");
+ let elementIds = ["locationBarGroupHeader", "locationBarSuggestionLabel"];
+ for (let id of elementIds) {
+ let element = document.getElementById(id);
+ element.dataset.l10nId = element.dataset.l10nIdOriginal;
+ delete element.dataset.l10nIdOriginal;
+ document.l10n.translateElements([element]);
+ }
+ document
+ .getElementById("openSearchEnginePreferences")
+ .classList.remove("extraMargin");
+ }
+ },
+
+ /**
+ * Updates the Firefox Suggest info box (in the address bar section) depending
+ * on the states of the Firefox Suggest toggles.
+ */
+ _updateFirefoxSuggestInfoBox() {
+ let nonsponsored = Preferences.get(
+ "browser.urlbar.suggest.quicksuggest.nonsponsored"
+ ).value;
+ let sponsored = Preferences.get(
+ "browser.urlbar.suggest.quicksuggest.sponsored"
+ ).value;
+ let dataCollection = Preferences.get(
+ "browser.urlbar.quicksuggest.dataCollection.enabled"
+ ).value;
+
+ // Get the l10n ID of the appropriate text based on the values of the three
+ // prefs.
+ let l10nId;
+ if (nonsponsored && sponsored && dataCollection) {
+ l10nId = "addressbar-firefox-suggest-info-all";
+ } else if (nonsponsored && sponsored && !dataCollection) {
+ l10nId = "addressbar-firefox-suggest-info-nonsponsored-sponsored";
+ } else if (nonsponsored && !sponsored && dataCollection) {
+ l10nId = "addressbar-firefox-suggest-info-nonsponsored-data";
+ } else if (nonsponsored && !sponsored && !dataCollection) {
+ l10nId = "addressbar-firefox-suggest-info-nonsponsored";
+ } else if (!nonsponsored && sponsored && dataCollection) {
+ l10nId = "addressbar-firefox-suggest-info-sponsored-data";
+ } else if (!nonsponsored && sponsored && !dataCollection) {
+ l10nId = "addressbar-firefox-suggest-info-sponsored";
+ } else if (!nonsponsored && !sponsored && dataCollection) {
+ l10nId = "addressbar-firefox-suggest-info-data";
+ }
+
+ let instance = (this._firefoxSuggestInfoBoxInstance = {});
+ let infoBox = document.getElementById("firefoxSuggestInfoBox");
+ if (!l10nId) {
+ infoBox.hidden = true;
+ } else {
+ let infoText = document.getElementById("firefoxSuggestInfoText");
+ infoText.dataset.l10nId = l10nId;
+
+ // If the info box is currently hidden and we unhide it immediately, it
+ // will show its old text until the new text is asyncly fetched and shown.
+ // That's ugly, so wait for the fetch to finish before unhiding it.
+ document.l10n.translateElements([infoText]).then(() => {
+ if (instance == this._firefoxSuggestInfoBoxInstance) {
+ infoBox.hidden = false;
+ }
+ });
+ }
+ },
+
+ /**
+ * Enables/disables the "Restore" button for dismissed Firefox Suggest
+ * suggestions.
+ */
+ _updateDismissedSuggestionsStatus() {
+ document.getElementById("restoreDismissedSuggestions").disabled =
+ !Services.prefs.prefHasUserValue(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST) &&
+ !(
+ Services.prefs.prefHasUserValue(PREF_URLBAR_WEATHER_USER_ENABLED) &&
+ !Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED)
+ );
+ },
+
+ /**
+ * Restores Firefox Suggest suggestions dismissed by the user.
+ */
+ restoreDismissedSuggestions() {
+ Services.prefs.clearUserPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST);
+ Services.prefs.clearUserPref(PREF_URLBAR_WEATHER_USER_ENABLED);
+ },
+
+ // GEOLOCATION
+
+ /**
+ * Displays the location exceptions dialog where specific site location
+ * preferences can be set.
+ */
+ showLocationExceptions() {
+ let params = { permissionType: "geo" };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // XR
+
+ /**
+ * Displays the XR exceptions dialog where specific site XR
+ * preferences can be set.
+ */
+ showXRExceptions() {
+ let params = { permissionType: "xr" };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // CAMERA
+
+ /**
+ * Displays the camera exceptions dialog where specific site camera
+ * preferences can be set.
+ */
+ showCameraExceptions() {
+ let params = { permissionType: "camera" };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // MICROPHONE
+
+ /**
+ * Displays the microphone exceptions dialog where specific site microphone
+ * preferences can be set.
+ */
+ showMicrophoneExceptions() {
+ let params = { permissionType: "microphone" };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // SPEAKER
+
+ /**
+ * Displays the speaker exceptions dialog where specific site speaker
+ * preferences can be set.
+ */
+ showSpeakerExceptions() {
+ let params = { permissionType: "speaker" };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // NOTIFICATIONS
+
+ /**
+ * Displays the notifications exceptions dialog where specific site notification
+ * preferences can be set.
+ */
+ showNotificationExceptions() {
+ let params = { permissionType: "desktop-notification" };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // MEDIA
+
+ showAutoplayMediaExceptions() {
+ var params = { permissionType: "autoplay-media" };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // POP-UPS
+
+ /**
+ * Displays the popup exceptions dialog where specific site popup preferences
+ * can be set.
+ */
+ showPopupExceptions() {
+ var params = {
+ blockVisible: false,
+ sessionVisible: false,
+ allowVisible: true,
+ prefilledHost: "",
+ permissionType: "popup",
+ };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml",
+ { features: "resizable=yes" },
+ params
+ );
+ },
+
+ // UTILITY FUNCTIONS
+
+ /**
+ * Utility function to enable/disable the button specified by aButtonID based
+ * on the value of the Boolean preference specified by aPreferenceID.
+ */
+ updateButtons(aButtonID, aPreferenceID) {
+ var button = document.getElementById(aButtonID);
+ var preference = Preferences.get(aPreferenceID);
+ button.disabled = !preference.value || preference.locked;
+ return undefined;
+ },
+
+ // BEGIN UI CODE
+
+ /*
+ * Preferences:
+ *
+ * dom.disable_open_during_load
+ * - true if popups are blocked by default, false otherwise
+ */
+
+ // POP-UPS
+
+ /**
+ * Displays a dialog in which the user can view and modify the list of sites
+ * where passwords are never saved.
+ */
+ showPasswordExceptions() {
+ var params = {
+ blockVisible: true,
+ sessionVisible: false,
+ allowVisible: false,
+ hideStatusColumn: true,
+ prefilledHost: "",
+ permissionType: "login-saving",
+ };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml",
+ undefined,
+ params
+ );
+ },
+
+ /**
+ * Initializes master password UI: the "use master password" checkbox, selects
+ * the master password button to show, and enables/disables it as necessary.
+ * The master password is controlled by various bits of NSS functionality, so
+ * the UI for it can't be controlled by the normal preference bindings.
+ */
+ _initMasterPasswordUI() {
+ var noMP = !LoginHelper.isPrimaryPasswordSet();
+
+ var button = document.getElementById("changeMasterPassword");
+ button.disabled = noMP;
+
+ var checkbox = document.getElementById("useMasterPassword");
+ checkbox.checked = !noMP;
+ checkbox.disabled =
+ (noMP && !Services.policies.isAllowed("createMasterPassword")) ||
+ (!noMP && !Services.policies.isAllowed("removeMasterPassword"));
+ },
+
+ /**
+ * Enables/disables the master password button depending on the state of the
+ * "use master password" checkbox, and prompts for master password removal if
+ * one is set.
+ */
+ async updateMasterPasswordButton() {
+ var checkbox = document.getElementById("useMasterPassword");
+ var button = document.getElementById("changeMasterPassword");
+ button.disabled = !checkbox.checked;
+
+ // unchecking the checkbox should try to immediately remove the master
+ // password, because it's impossible to non-destructively remove the master
+ // password used to encrypt all the passwords without providing it (by
+ // design), and it would be extremely odd to pop up that dialog when the
+ // user closes the prefwindow and saves his settings
+ if (!checkbox.checked) {
+ await this._removeMasterPassword();
+ } else {
+ await this.changeMasterPassword();
+ }
+
+ this._initMasterPasswordUI();
+ },
+
+ /**
+ * Displays the "remove master password" dialog to allow the user to remove
+ * the current master password. When the dialog is dismissed, master password
+ * UI is automatically updated.
+ */
+ async _removeMasterPassword() {
+ var secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(
+ Ci.nsIPKCS11ModuleDB
+ );
+ if (secmodDB.isFIPSEnabled) {
+ let title = document.getElementById("fips-title").textContent;
+ let desc = document.getElementById("fips-desc").textContent;
+ Services.prompt.alert(window, title, desc);
+ this._initMasterPasswordUI();
+ } else {
+ gSubDialog.open("chrome://mozapps/content/preferences/removemp.xhtml", {
+ closingCallback: this._initMasterPasswordUI.bind(this),
+ });
+ }
+ },
+
+ /**
+ * Displays a dialog in which the primary password may be changed.
+ */
+ async changeMasterPassword() {
+ // Require OS authentication before the user can set a Primary Password.
+ // OS reauthenticate functionality is not available on Linux yet (bug 1527745)
+ if (
+ !LoginHelper.isPrimaryPasswordSet() &&
+ OS_AUTH_ENABLED &&
+ OSKeyStore.canReauth()
+ ) {
+ // Uses primary-password-os-auth-dialog-message-win and
+ // primary-password-os-auth-dialog-message-macosx via concatenation:
+ let messageId =
+ "primary-password-os-auth-dialog-message-" + AppConstants.platform;
+ let [messageText, captionText] = await document.l10n.formatMessages([
+ {
+ id: messageId,
+ },
+ {
+ id: "master-password-os-auth-dialog-caption",
+ },
+ ]);
+ let win = Services.wm.getMostRecentBrowserWindow();
+ let loggedIn = await OSKeyStore.ensureLoggedIn(
+ messageText.value,
+ captionText.value,
+ win,
+ false
+ );
+ if (!loggedIn.authenticated) {
+ return;
+ }
+ }
+
+ gSubDialog.open("chrome://mozapps/content/preferences/changemp.xhtml", {
+ features: "resizable=no",
+ closingCallback: this._initMasterPasswordUI.bind(this),
+ });
+ },
+
+ /**
+ * Set up the initial state for the password generation UI.
+ * It will be hidden unless the .available pref is true
+ */
+ _initPasswordGenerationUI() {
+ // we don't watch the .available pref for runtime changes
+ let prefValue = Services.prefs.getBoolPref(
+ PREF_PASSWORD_GENERATION_AVAILABLE,
+ false
+ );
+ document.getElementById("generatePasswordsBox").hidden = !prefValue;
+ },
+
+ toggleRelayIntegration() {
+ const checkbox = document.getElementById("relayIntegration");
+ if (checkbox.checked) {
+ FirefoxRelay.markAsAvailable();
+ FirefoxRelayTelemetry.recordRelayPrefEvent("enabled");
+ } else {
+ FirefoxRelay.markAsDisabled();
+ FirefoxRelayTelemetry.recordRelayPrefEvent("disabled");
+ }
+ },
+
+ _updateRelayIntegrationUI() {
+ document.getElementById("relayIntegrationBox").hidden =
+ !FirefoxRelay.isAvailable;
+ document.getElementById("relayIntegration").checked =
+ FirefoxRelay.isAvailable && !FirefoxRelay.isDisabled;
+ },
+
+ _initRelayIntegrationUI() {
+ document
+ .getElementById("relayIntegrationLearnMoreLink")
+ .setAttribute("href", FirefoxRelay.learnMoreUrl);
+
+ setEventListener(
+ "relayIntegration",
+ "command",
+ gPrivacyPane.toggleRelayIntegration.bind(gPrivacyPane)
+ );
+ Preferences.get("signon.firefoxRelay.feature").on(
+ "change",
+ gPrivacyPane._updateRelayIntegrationUI.bind(gPrivacyPane)
+ );
+
+ this._updateRelayIntegrationUI();
+ },
+
+ /**
+ * Shows the sites where the user has saved passwords and the associated login
+ * information.
+ */
+ showPasswords() {
+ let loginManager = window.windowGlobalChild.getActor("LoginManager");
+ loginManager.sendAsyncMessage("PasswordManager:OpenPreferences", {
+ entryPoint: "preferences",
+ });
+ },
+
+ /**
+ * Enables/disables dependent controls related to password saving
+ * When password saving is not enabled, we need to also disable the password generation checkbox
+ * The Exceptions button is used to configure sites where passwords are never saved.
+ */
+ readSavePasswords() {
+ var prefValue = Preferences.get("signon.rememberSignons").value;
+ document.getElementById("passwordExceptions").disabled = !prefValue;
+ document.getElementById("generatePasswords").disabled = !prefValue;
+ document.getElementById("passwordAutofillCheckbox").disabled = !prefValue;
+ document.getElementById("relayIntegration").disabled =
+ !prefValue || Services.prefs.prefIsLocked("signon.firefoxRelay.feature");
+ // don't override pref value in UI
+ return undefined;
+ },
+
+ /**
+ * Initalizes pref listeners for the password manager.
+ *
+ * This ensures that the user is always notified if an extension is controlling the password manager.
+ */
+ initListenersForExtensionControllingPasswordManager() {
+ this._passwordManagerCheckbox = document.getElementById("savePasswords");
+ this._disableExtensionButton = document.getElementById(
+ "disablePasswordManagerExtension"
+ );
+
+ this._disableExtensionButton.addEventListener(
+ "command",
+ makeDisableControllingExtension(
+ PREF_SETTING_TYPE,
+ PASSWORD_MANAGER_PREF_ID
+ )
+ );
+
+ initListenersForPrefChange(
+ PREF_SETTING_TYPE,
+ PASSWORD_MANAGER_PREF_ID,
+ this._passwordManagerCheckbox
+ );
+ },
+
+ /**
+ * Enables/disables the add-ons Exceptions button depending on whether
+ * or not add-on installation warnings are displayed.
+ */
+ readWarnAddonInstall() {
+ var warn = Preferences.get("xpinstall.whitelist.required");
+ var exceptions = document.getElementById("addonExceptions");
+
+ exceptions.disabled = !warn.value || warn.locked;
+
+ // don't override the preference value
+ return undefined;
+ },
+
+ _initSafeBrowsing() {
+ let enableSafeBrowsing = document.getElementById("enableSafeBrowsing");
+ let blockDownloads = document.getElementById("blockDownloads");
+ let blockUncommonUnwanted = document.getElementById(
+ "blockUncommonUnwanted"
+ );
+
+ let safeBrowsingPhishingPref = Preferences.get(
+ "browser.safebrowsing.phishing.enabled"
+ );
+ let safeBrowsingMalwarePref = Preferences.get(
+ "browser.safebrowsing.malware.enabled"
+ );
+
+ let blockDownloadsPref = Preferences.get(
+ "browser.safebrowsing.downloads.enabled"
+ );
+ let malwareTable = Preferences.get("urlclassifier.malwareTable");
+
+ let blockUnwantedPref = Preferences.get(
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+ );
+ let blockUncommonPref = Preferences.get(
+ "browser.safebrowsing.downloads.remote.block_uncommon"
+ );
+
+ enableSafeBrowsing.addEventListener("command", function () {
+ safeBrowsingPhishingPref.value = enableSafeBrowsing.checked;
+ safeBrowsingMalwarePref.value = enableSafeBrowsing.checked;
+
+ blockDownloads.disabled =
+ !enableSafeBrowsing.checked || blockDownloadsPref.locked;
+ blockUncommonUnwanted.disabled =
+ !blockDownloads.checked ||
+ !enableSafeBrowsing.checked ||
+ blockUnwantedPref.locked ||
+ blockUncommonPref.locked;
+ });
+
+ blockDownloads.addEventListener("command", function () {
+ blockDownloadsPref.value = blockDownloads.checked;
+ blockUncommonUnwanted.disabled =
+ !blockDownloads.checked ||
+ blockUnwantedPref.locked ||
+ blockUncommonPref.locked;
+ });
+
+ blockUncommonUnwanted.addEventListener("command", function () {
+ blockUnwantedPref.value = blockUncommonUnwanted.checked;
+ blockUncommonPref.value = blockUncommonUnwanted.checked;
+
+ let malware = malwareTable.value
+ .split(",")
+ .filter(
+ x =>
+ x !== "goog-unwanted-proto" &&
+ x !== "goog-unwanted-shavar" &&
+ x !== "moztest-unwanted-simple"
+ );
+
+ if (blockUncommonUnwanted.checked) {
+ if (malware.includes("goog-malware-shavar")) {
+ malware.push("goog-unwanted-shavar");
+ } else {
+ malware.push("goog-unwanted-proto");
+ }
+
+ malware.push("moztest-unwanted-simple");
+ }
+
+ // sort alphabetically to keep the pref consistent
+ malware.sort();
+
+ malwareTable.value = malware.join(",");
+
+ // Force an update after changing the malware table.
+ listManager.forceUpdates(malwareTable.value);
+ });
+
+ // set initial values
+
+ enableSafeBrowsing.checked =
+ safeBrowsingPhishingPref.value && safeBrowsingMalwarePref.value;
+ if (!enableSafeBrowsing.checked) {
+ blockDownloads.setAttribute("disabled", "true");
+ blockUncommonUnwanted.setAttribute("disabled", "true");
+ }
+
+ blockDownloads.checked = blockDownloadsPref.value;
+ if (!blockDownloadsPref.value) {
+ blockUncommonUnwanted.setAttribute("disabled", "true");
+ }
+ blockUncommonUnwanted.checked =
+ blockUnwantedPref.value && blockUncommonPref.value;
+
+ if (safeBrowsingPhishingPref.locked || safeBrowsingMalwarePref.locked) {
+ enableSafeBrowsing.disabled = true;
+ }
+ if (blockDownloadsPref.locked) {
+ blockDownloads.disabled = true;
+ }
+ if (blockUnwantedPref.locked || blockUncommonPref.locked) {
+ blockUncommonUnwanted.disabled = true;
+ }
+ },
+
+ /**
+ * Displays the exceptions lists for add-on installation warnings.
+ */
+ showAddonExceptions() {
+ var params = this._addonParams;
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml",
+ undefined,
+ params
+ );
+ },
+
+ /**
+ * Parameters for the add-on install permissions dialog.
+ */
+ _addonParams: {
+ blockVisible: false,
+ sessionVisible: false,
+ allowVisible: true,
+ prefilledHost: "",
+ permissionType: "install",
+ },
+
+ /**
+ * readEnableOCSP is used by the preferences UI to determine whether or not
+ * the checkbox for OCSP fetching should be checked (it returns true if it
+ * should be checked and false otherwise). The about:config preference
+ * "security.OCSP.enabled" is an integer rather than a boolean, so it can't be
+ * directly mapped from {true,false} to {checked,unchecked}. The possible
+ * values for "security.OCSP.enabled" are:
+ * 0: fetching is disabled
+ * 1: fetch for all certificates
+ * 2: fetch only for EV certificates
+ * Hence, if "security.OCSP.enabled" is non-zero, the checkbox should be
+ * checked. Otherwise, it should be unchecked.
+ */
+ readEnableOCSP() {
+ var preference = Preferences.get("security.OCSP.enabled");
+ // This is the case if the preference is the default value.
+ if (preference.value === undefined) {
+ return true;
+ }
+ return preference.value != 0;
+ },
+
+ /**
+ * writeEnableOCSP is used by the preferences UI to map the checked/unchecked
+ * state of the OCSP fetching checkbox to the value that the preference
+ * "security.OCSP.enabled" should be set to (it returns that value). See the
+ * readEnableOCSP documentation for more background. We unfortunately don't
+ * have enough information to map from {true,false} to all possible values for
+ * "security.OCSP.enabled", but a reasonable alternative is to map from
+ * {true,false} to {<the default value>,0}. That is, if the box is checked,
+ * "security.OCSP.enabled" will be set to whatever default it should be, given
+ * the platform and channel. If the box is unchecked, the preference will be
+ * set to 0. Obviously this won't work if the default is 0, so we will have to
+ * revisit this if we ever set it to 0.
+ */
+ writeEnableOCSP() {
+ var checkbox = document.getElementById("enableOCSP");
+ var defaults = Services.prefs.getDefaultBranch(null);
+ var defaultValue = defaults.getIntPref("security.OCSP.enabled");
+ return checkbox.checked ? defaultValue : 0;
+ },
+
+ /**
+ * Displays the user's certificates and associated options.
+ */
+ showCertificates() {
+ gSubDialog.open("chrome://pippki/content/certManager.xhtml");
+ },
+
+ /**
+ * Displays a dialog from which the user can manage his security devices.
+ */
+ showSecurityDevices() {
+ gSubDialog.open("chrome://pippki/content/device_manager.xhtml");
+ },
+
+ /**
+ * Displays the learn more health report page when a user opts out of data collection.
+ */
+ showDataDeletion() {
+ let url =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "telemetry-clientid";
+ window.open(url, "_blank");
+ },
+
+ initDataCollection() {
+ if (
+ !AppConstants.MOZ_DATA_REPORTING &&
+ !NimbusFeatures.majorRelease2022.getVariable(
+ "feltPrivacyShowPreferencesSection"
+ )
+ ) {
+ // Nothing to control in the data collection section, remove it.
+ document.getElementById("dataCollectionCategory").remove();
+ document.getElementById("dataCollectionGroup").remove();
+ return;
+ }
+
+ this._setupLearnMoreLink(
+ "toolkit.datacollection.infoURL",
+ "dataCollectionPrivacyNotice"
+ );
+ this.initPrivacySegmentation();
+ },
+
+ initSubmitCrashes() {
+ this._setupLearnMoreLink(
+ "toolkit.crashreporter.infoURL",
+ "crashReporterLearnMore"
+ );
+ setEventListener("crashReporterLabel", "click", function (event) {
+ if (event.target.localName == "a") {
+ return;
+ }
+ const checkboxId = event.target.getAttribute("for");
+ document.getElementById(checkboxId).click();
+ });
+ },
+
+ initPrivacySegmentation() {
+ // Section visibility
+ let section = document.getElementById("privacySegmentationSection");
+ let updatePrivacySegmentationSectionVisibilityState = () => {
+ section.hidden = !NimbusFeatures.majorRelease2022.getVariable(
+ "feltPrivacyShowPreferencesSection"
+ );
+ };
+
+ NimbusFeatures.majorRelease2022.onUpdate(
+ updatePrivacySegmentationSectionVisibilityState
+ );
+ window.addEventListener("unload", () => {
+ NimbusFeatures.majorRelease2022.offUpdate(
+ updatePrivacySegmentationSectionVisibilityState
+ );
+ });
+
+ updatePrivacySegmentationSectionVisibilityState();
+ },
+
+ /**
+ * Set up or hide the Learn More links for various data collection options
+ */
+ _setupLearnMoreLink(pref, element) {
+ // set up the Learn More link with the correct URL
+ let url = Services.urlFormatter.formatURLPref(pref);
+ let el = document.getElementById(element);
+
+ if (url) {
+ el.setAttribute("href", url);
+ } else {
+ el.hidden = true;
+ }
+ },
+
+ /**
+ * Initialize the health report service reference and checkbox.
+ */
+ initSubmitHealthReport() {
+ this._setupLearnMoreLink(
+ "datareporting.healthreport.infoURL",
+ "FHRLearnMore"
+ );
+
+ let checkbox = document.getElementById("submitHealthReportBox");
+
+ // Telemetry is only sending data if MOZ_TELEMETRY_REPORTING is defined.
+ // We still want to display the preferences panel if that's not the case, but
+ // we want it to be disabled and unchecked.
+ if (
+ Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED) ||
+ !AppConstants.MOZ_TELEMETRY_REPORTING
+ ) {
+ checkbox.setAttribute("disabled", "true");
+ return;
+ }
+
+ checkbox.checked =
+ Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED) &&
+ AppConstants.MOZ_TELEMETRY_REPORTING;
+ },
+
+ /**
+ * Update the health report preference with state from checkbox.
+ */
+ updateSubmitHealthReport() {
+ let checkbox = document.getElementById("submitHealthReportBox");
+ let telemetryContainer = document.getElementById("telemetry-container");
+
+ Services.prefs.setBoolPref(PREF_UPLOAD_ENABLED, checkbox.checked);
+ telemetryContainer.hidden = checkbox.checked;
+ },
+
+ /**
+ * Initialize the opt-out-study preference checkbox into about:preferences and
+ * handles events coming from the UI for it.
+ */
+ initOptOutStudyCheckbox(doc) {
+ // The checkbox should be disabled if any of the below are true. This
+ // prevents the user from changing the value in the box.
+ //
+ // * the policy forbids shield
+ // * Normandy is disabled
+ //
+ // The checkbox should match the value of the preference only if all of
+ // these are true. Otherwise, the checkbox should remain unchecked. This
+ // is because in these situations, Shield studies are always disabled, and
+ // so showing a checkbox would be confusing.
+ //
+ // * the policy allows Shield
+ // * Normandy is enabled
+
+ const allowedByPolicy = Services.policies.isAllowed("Shield");
+ const checkbox = document.getElementById("optOutStudiesEnabled");
+
+ if (
+ allowedByPolicy &&
+ Services.prefs.getBoolPref(PREF_NORMANDY_ENABLED, false)
+ ) {
+ if (Services.prefs.getBoolPref(PREF_OPT_OUT_STUDIES_ENABLED, false)) {
+ checkbox.setAttribute("checked", "true");
+ } else {
+ checkbox.removeAttribute("checked");
+ }
+ checkbox.setAttribute("preference", PREF_OPT_OUT_STUDIES_ENABLED);
+ checkbox.removeAttribute("disabled");
+ } else {
+ checkbox.removeAttribute("preference");
+ checkbox.removeAttribute("checked");
+ checkbox.setAttribute("disabled", "true");
+ }
+ },
+
+ initAddonRecommendationsCheckbox() {
+ // Setup the checkbox.
+ dataCollectionCheckboxHandler({
+ checkbox: document.getElementById("addonRecommendationEnabled"),
+ pref: PREF_ADDON_RECOMMENDATIONS_ENABLED,
+ });
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "sitedatamanager:updating-sites":
+ // While updating, we want to disable this section and display loading message until updated
+ this.toggleSiteData(false);
+ this.showSiteDataLoading();
+ break;
+
+ case "sitedatamanager:sites-updated":
+ this.toggleSiteData(true);
+ SiteDataManager.getTotalUsage().then(
+ this.updateTotalDataSizeLabel.bind(this)
+ );
+ break;
+ case "network:trr-uri-changed":
+ case "network:trr-mode-changed":
+ case "network:trr-confirmation":
+ gPrivacyPane.updateDoHStatus();
+ break;
+ }
+ },
+};
diff --git a/browser/components/preferences/search.inc.xhtml b/browser/components/preferences/search.inc.xhtml
new file mode 100644
index 0000000000..b96af3c2f1
--- /dev/null
+++ b/browser/components/preferences/search.inc.xhtml
@@ -0,0 +1,118 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+ <script src="chrome://browser/content/preferences/search.js"/>
+ <html:template id="template-paneSearch">
+ <hbox id="searchCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneSearch">
+ <html:h1 data-l10n-id="pane-search-title"/>
+ </hbox>
+
+ <groupbox id="searchbarGroup" data-category="paneSearch" hidden="true">
+ <label control="searchBarVisibleGroup"><html:h2 data-l10n-id="search-bar-header"/></label>
+ <radiogroup id="searchBarVisibleGroup" preference="browser.search.widget.inNavBar" align="start">
+ <radio id="searchBarHiddenRadio" value="false" data-l10n-id="search-bar-hidden"/>
+ <image class="searchBarImage searchBarHiddenImage" role="presentation"/>
+ <checkbox id="searchShowSearchTermCheckbox"
+ data-l10n-id="search-show-search-term-option"
+ preference="browser.urlbar.showSearchTerms.enabled"
+ hidden="true" />
+ <radio id="searchBarShownRadio" value="true" data-l10n-id="search-bar-shown"/>
+ <image class="searchBarImage searchBarShownImage" role="presentation"/>
+ </radiogroup>
+ </groupbox>
+
+ <!-- Default Search Engine -->
+ <groupbox id="defaultEngineGroup" data-category="paneSearch" hidden="true">
+ <label><html:h2 data-l10n-id="search-engine-default-header" /></label>
+ <description data-l10n-id="search-engine-default-desc-2" />
+ <hbox>
+ <menulist id="defaultEngine">
+ <menupopup/>
+ </menulist>
+ </hbox>
+ <checkbox id="browserSeparateDefaultEngine"
+ data-l10n-id="search-separate-default-engine"
+ hidden="true"/>
+ <vbox id="browserPrivateEngineSelection" class="indent" hidden="true">
+ <description data-l10n-id="search-engine-default-private-desc-2" />
+ <hbox>
+ <menulist id="defaultPrivateEngine">
+ <menupopup/>
+ </menulist>
+ </hbox>
+ </vbox>
+ </groupbox>
+
+ <groupbox id="searchSuggestionsGroup" data-category="paneSearch" hidden="true">
+ <label><html:h2 data-l10n-id="search-suggestions-header" /></label>
+ <description id="searchSuggestionsDesc"
+ data-l10n-id="search-suggestions-desc" />
+
+ <checkbox id="suggestionsInSearchFieldsCheckbox"
+ data-l10n-id="search-suggestions-option"
+ preference="browser.search.suggest.enabled"/>
+ <vbox class="indent">
+ <checkbox id="urlBarSuggestion" data-l10n-id="search-show-suggestions-url-bar-option" />
+ <checkbox id="showSearchSuggestionsFirstCheckbox"
+ data-l10n-id="search-show-suggestions-above-history-option"
+ preference="browser.urlbar.showSearchSuggestionsFirst"/>
+ <checkbox id="showSearchSuggestionsPrivateWindows"
+ data-l10n-id="search-show-suggestions-private-windows"/>
+ <hbox id="urlBarSuggestionPermanentPBLabel"
+ align="center" class="indent">
+ <label flex="1" data-l10n-id="search-suggestions-cant-show" />
+ </hbox>
+ </vbox>
+ <label id="openLocationBarPrivacyPreferences" is="text-link"
+ data-l10n-id="suggestions-addressbar-settings-generic2"/>
+ </groupbox>
+
+ <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch" hidden="true">
+ <label><html:h2 data-l10n-id="search-one-click-header2" /></label>
+ <description data-l10n-id="search-one-click-desc" />
+
+ <tree id="engineList" flex="1" rows="11" hidecolumnpicker="true" editable="true"
+ seltype="single" allowunderflowscroll="true">
+ <treechildren id="engineChildren" flex="1"/>
+ <treecols>
+ <treecol id="engineShown" type="checkbox" editable="true" sortable="false"/>
+ <treecol id="engineName" flex="1" data-l10n-id="search-choose-engine-column" sortable="false"/>
+ <treecol id="engineKeyword" flex="1" data-l10n-id="search-choose-keyword-column" sortable="false"/>
+ </treecols>
+ </tree>
+
+ <hbox>
+ <button id="restoreDefaultSearchEngines"
+ is="highlightable-button"
+ data-l10n-id="search-restore-default"
+ />
+ <spacer flex="1"/>
+ <button id="removeEngineButton"
+ is="highlightable-button"
+ class="searchEngineAction"
+ data-l10n-id="search-remove-engine"
+ disabled="true"
+ />
+ <button id="addEngineButton"
+ is="highlightable-button"
+ class="searchEngineAction"
+ hidden="true"
+ data-l10n-id="search-add-engine"
+ search-l10n-ids="
+ add-engine-button,
+ add-engine-name,
+ add-engine-alias,
+ add-engine-url,
+ add-engine-dialog.buttonlabelaccept,
+ "
+ />
+ </hbox>
+ <hbox id="addEnginesBox" pack="start">
+ <label id="addEngines" data-l10n-id="search-find-more-link" is="text-link"></label>
+ </hbox>
+ </groupbox>
+ </html:template>
diff --git a/browser/components/preferences/search.js b/browser/components/preferences/search.js
new file mode 100644
index 0000000000..52f49b77bb
--- /dev/null
+++ b/browser/components/preferences/search.js
@@ -0,0 +1,1100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
+});
+
+Preferences.addAll([
+ { id: "browser.search.suggest.enabled", type: "bool" },
+ { id: "browser.urlbar.suggest.searches", type: "bool" },
+ { id: "browser.search.suggest.enabled.private", type: "bool" },
+ { id: "browser.search.hiddenOneOffs", type: "unichar" },
+ { id: "browser.search.widget.inNavBar", type: "bool" },
+ { id: "browser.urlbar.showSearchSuggestionsFirst", type: "bool" },
+ { id: "browser.urlbar.showSearchTerms.enabled", type: "bool" },
+ { id: "browser.search.separatePrivateDefault", type: "bool" },
+ { id: "browser.search.separatePrivateDefault.ui.enabled", type: "bool" },
+]);
+
+const ENGINE_FLAVOR = "text/x-moz-search-engine";
+const SEARCH_TYPE = "default_search";
+const SEARCH_KEY = "defaultSearch";
+
+var gEngineView = null;
+
+var gSearchPane = {
+ init() {
+ gEngineView = new EngineView(new EngineStore());
+ document.getElementById("engineList").view = gEngineView;
+ this.buildDefaultEngineDropDowns().catch(console.error);
+
+ if (
+ Services.policies &&
+ !Services.policies.isAllowed("installSearchEngine")
+ ) {
+ document.getElementById("addEnginesBox").hidden = true;
+ } else {
+ let addEnginesLink = document.getElementById("addEngines");
+ addEnginesLink.setAttribute("href", lazy.SearchUIUtils.searchEnginesURL);
+ }
+
+ window.addEventListener("click", this);
+ window.addEventListener("command", this);
+ window.addEventListener("dragstart", this);
+ window.addEventListener("keypress", this);
+ window.addEventListener("select", this);
+ window.addEventListener("dblclick", this);
+
+ Services.obs.addObserver(this, "browser-search-engine-modified");
+ Services.obs.addObserver(this, "intl:app-locales-changed");
+ window.addEventListener("unload", () => {
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ Services.obs.removeObserver(this, "intl:app-locales-changed");
+ });
+
+ let suggestsPref = Preferences.get("browser.search.suggest.enabled");
+ let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches");
+ let privateSuggestsPref = Preferences.get(
+ "browser.search.suggest.enabled.private"
+ );
+ let updateSuggestionCheckboxes =
+ this._updateSuggestionCheckboxes.bind(this);
+ suggestsPref.on("change", updateSuggestionCheckboxes);
+ urlbarSuggestsPref.on("change", updateSuggestionCheckboxes);
+ let urlbarSuggests = document.getElementById("urlBarSuggestion");
+ urlbarSuggests.addEventListener("command", () => {
+ urlbarSuggestsPref.value = urlbarSuggests.checked;
+ });
+ let privateWindowCheckbox = document.getElementById(
+ "showSearchSuggestionsPrivateWindows"
+ );
+ privateWindowCheckbox.addEventListener("command", () => {
+ privateSuggestsPref.value = privateWindowCheckbox.checked;
+ });
+
+ setEventListener(
+ "browserSeparateDefaultEngine",
+ "command",
+ this._onBrowserSeparateDefaultEngineChange.bind(this)
+ );
+ setEventListener(
+ "openLocationBarPrivacyPreferences",
+ "click",
+ function (event) {
+ if (event.button == 0) {
+ gotoPref("privacy-locationBar");
+ }
+ }
+ );
+
+ this._initDefaultEngines();
+ this._initShowSearchTermsCheckbox();
+ this._updateSuggestionCheckboxes();
+ this._showAddEngineButton();
+ },
+
+ /**
+ * Initialize the default engine handling. This will hide the private default
+ * options if they are not enabled yet.
+ */
+ _initDefaultEngines() {
+ this._separatePrivateDefaultEnabledPref = Preferences.get(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+
+ this._separatePrivateDefaultPref = Preferences.get(
+ "browser.search.separatePrivateDefault"
+ );
+
+ const checkbox = document.getElementById("browserSeparateDefaultEngine");
+ checkbox.checked = !this._separatePrivateDefaultPref.value;
+
+ this._updatePrivateEngineDisplayBoxes();
+
+ const listener = () => {
+ this._updatePrivateEngineDisplayBoxes();
+ this.buildDefaultEngineDropDowns().catch(console.error);
+ };
+
+ this._separatePrivateDefaultEnabledPref.on("change", listener);
+ this._separatePrivateDefaultPref.on("change", listener);
+ },
+
+ _initShowSearchTermsCheckbox() {
+ let checkbox = document.getElementById("searchShowSearchTermCheckbox");
+
+ // Add Nimbus event to show/hide checkbox.
+ let onNimbus = () => {
+ checkbox.hidden = !UrlbarPrefs.get("showSearchTermsFeatureGate");
+ };
+ NimbusFeatures.urlbar.onUpdate(onNimbus);
+
+ // Add observer of Search Bar preference as showSearchTerms
+ // can't be enabled/disabled while Search Bar is enabled.
+ let searchBarPref = Preferences.get("browser.search.widget.inNavBar");
+ let updateCheckboxEnabled = () => {
+ checkbox.disabled = searchBarPref.value;
+ };
+ searchBarPref.on("change", updateCheckboxEnabled);
+
+ // Fire once to initialize.
+ onNimbus();
+ updateCheckboxEnabled();
+
+ window.addEventListener("unload", () => {
+ NimbusFeatures.urlbar.offUpdate(onNimbus);
+ });
+ },
+
+ _updatePrivateEngineDisplayBoxes() {
+ const separateEnabled = this._separatePrivateDefaultEnabledPref.value;
+ document.getElementById("browserSeparateDefaultEngine").hidden =
+ !separateEnabled;
+
+ const separateDefault = this._separatePrivateDefaultPref.value;
+
+ const vbox = document.getElementById("browserPrivateEngineSelection");
+ vbox.hidden = !separateEnabled || !separateDefault;
+ },
+
+ _onBrowserSeparateDefaultEngineChange(event) {
+ this._separatePrivateDefaultPref.value = !event.target.checked;
+ },
+
+ _updateSuggestionCheckboxes() {
+ let suggestsPref = Preferences.get("browser.search.suggest.enabled");
+ let permanentPB = Services.prefs.getBoolPref(
+ "browser.privatebrowsing.autostart"
+ );
+ let urlbarSuggests = document.getElementById("urlBarSuggestion");
+ let positionCheckbox = document.getElementById(
+ "showSearchSuggestionsFirstCheckbox"
+ );
+ let privateWindowCheckbox = document.getElementById(
+ "showSearchSuggestionsPrivateWindows"
+ );
+
+ urlbarSuggests.disabled = !suggestsPref.value || permanentPB;
+ privateWindowCheckbox.disabled = !suggestsPref.value;
+ privateWindowCheckbox.checked = Preferences.get(
+ "browser.search.suggest.enabled.private"
+ ).value;
+ if (privateWindowCheckbox.disabled) {
+ privateWindowCheckbox.checked = false;
+ }
+
+ let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches");
+ urlbarSuggests.checked = urlbarSuggestsPref.value;
+ if (urlbarSuggests.disabled) {
+ urlbarSuggests.checked = false;
+ }
+
+ if (urlbarSuggests.checked) {
+ positionCheckbox.disabled = false;
+ // Update the checked state of the show-suggestions-first checkbox. Note
+ // that this does *not* also update its pref, it only checks the box.
+ positionCheckbox.checked = Preferences.get(
+ positionCheckbox.getAttribute("preference")
+ ).value;
+ } else {
+ positionCheckbox.disabled = true;
+ positionCheckbox.checked = false;
+ }
+
+ let permanentPBLabel = document.getElementById(
+ "urlBarSuggestionPermanentPBLabel"
+ );
+ permanentPBLabel.hidden = urlbarSuggests.hidden || !permanentPB;
+ },
+
+ _showAddEngineButton() {
+ let aliasRefresh = Services.prefs.getBoolPref(
+ "browser.urlbar.update2.engineAliasRefresh",
+ false
+ );
+ if (aliasRefresh) {
+ let addButton = document.getElementById("addEngineButton");
+ addButton.hidden = false;
+ }
+ },
+
+ /**
+ * Builds the default and private engines drop down lists. This is called
+ * each time something affects the list of engines.
+ */
+ async buildDefaultEngineDropDowns() {
+ await this._buildEngineDropDown(
+ document.getElementById("defaultEngine"),
+ (
+ await Services.search.getDefault()
+ ).name,
+ false
+ );
+
+ if (this._separatePrivateDefaultEnabledPref.value) {
+ await this._buildEngineDropDown(
+ document.getElementById("defaultPrivateEngine"),
+ (
+ await Services.search.getDefaultPrivate()
+ ).name,
+ true
+ );
+ }
+ },
+
+ /**
+ * Builds a drop down menu of search engines.
+ *
+ * @param {DOMMenuList} list
+ * The menu list element to attach the list of engines.
+ * @param {string} currentEngine
+ * The name of the current default engine.
+ * @param {boolean} isPrivate
+ * True if we are dealing with the default engine for private mode.
+ */
+ async _buildEngineDropDown(list, currentEngine, isPrivate) {
+ // If the current engine isn't in the list any more, select the first item.
+ let engines = gEngineView._engineStore._engines;
+ if (!engines.length) {
+ return;
+ }
+ if (!engines.some(e => e.name == currentEngine)) {
+ currentEngine = engines[0].name;
+ }
+
+ // Now clean-up and rebuild the list.
+ list.removeAllItems();
+ gEngineView._engineStore._engines.forEach(e => {
+ let item = list.appendItem(e.name);
+ item.setAttribute(
+ "class",
+ "menuitem-iconic searchengine-menuitem menuitem-with-favicon"
+ );
+ if (e.iconURI) {
+ item.setAttribute("image", e.iconURI.spec);
+ }
+ item.engine = e;
+ if (e.name == currentEngine) {
+ list.selectedItem = item;
+ }
+ });
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "dblclick":
+ if (aEvent.target.id == "engineChildren") {
+ let cell = aEvent.target.parentNode.getCellAt(
+ aEvent.clientX,
+ aEvent.clientY
+ );
+ if (cell.col?.id == "engineKeyword") {
+ this.startEditingAlias(gEngineView.selectedIndex);
+ }
+ }
+ break;
+ case "click":
+ if (
+ aEvent.target.id != "engineChildren" &&
+ !aEvent.target.classList.contains("searchEngineAction")
+ ) {
+ let engineList = document.getElementById("engineList");
+ // We don't want to toggle off selection while editing keyword
+ // so proceed only when the input field is hidden.
+ // We need to check that engineList.view is defined here
+ // because the "click" event listener is on <window> and the
+ // view might have been destroyed if the pane has been navigated
+ // away from.
+ if (engineList.inputField.hidden && engineList.view) {
+ let selection = engineList.view.selection;
+ if (selection?.count > 0) {
+ selection.toggleSelect(selection.currentIndex);
+ }
+ engineList.blur();
+ }
+ }
+ break;
+ case "command":
+ switch (aEvent.target.id) {
+ case "":
+ if (
+ aEvent.target.parentNode &&
+ aEvent.target.parentNode.parentNode
+ ) {
+ if (aEvent.target.parentNode.parentNode.id == "defaultEngine") {
+ gSearchPane.setDefaultEngine();
+ } else if (
+ aEvent.target.parentNode.parentNode.id == "defaultPrivateEngine"
+ ) {
+ gSearchPane.setDefaultPrivateEngine();
+ }
+ }
+ break;
+ case "restoreDefaultSearchEngines":
+ gSearchPane.onRestoreDefaults();
+ break;
+ case "removeEngineButton":
+ Services.search.removeEngine(
+ gEngineView.selectedEngine.originalEngine
+ );
+ break;
+ case "addEngineButton":
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/addEngine.xhtml",
+ { features: "resizable=no, modal=yes" }
+ );
+ break;
+ }
+ break;
+ case "dragstart":
+ if (aEvent.target.id == "engineChildren") {
+ onDragEngineStart(aEvent);
+ }
+ break;
+ case "keypress":
+ if (aEvent.target.id == "engineList") {
+ gSearchPane.onTreeKeyPress(aEvent);
+ }
+ break;
+ case "select":
+ if (aEvent.target.id == "engineList") {
+ gSearchPane.onTreeSelect();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Handle when the app locale is changed.
+ */
+ async appLocalesChanged() {
+ await document.l10n.ready;
+ await gEngineView.loadL10nNames();
+ },
+
+ /**
+ * Update the default engine UI and engine tree view as appropriate when engine changes
+ * or locale changes occur.
+ *
+ * @param {Object} engine
+ * @param {string} data
+ */
+ browserSearchEngineModified(engine, data) {
+ engine.QueryInterface(Ci.nsISearchEngine);
+ switch (data) {
+ case "engine-added":
+ gEngineView._engineStore.addEngine(engine);
+ gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1);
+ gSearchPane.buildDefaultEngineDropDowns();
+ break;
+ case "engine-changed":
+ gSearchPane.buildDefaultEngineDropDowns();
+ gEngineView._engineStore.updateEngine(engine);
+ gEngineView.invalidate();
+ break;
+ case "engine-removed":
+ gSearchPane.remove(engine);
+ break;
+ case "engine-default": {
+ // If the user is going through the drop down using up/down keys, the
+ // dropdown may still be open (eg. on Windows) when engine-default is
+ // fired, so rebuilding the list unconditionally would get in the way.
+ let selectedEngine =
+ document.getElementById("defaultEngine").selectedItem.engine;
+ if (selectedEngine.name != engine.name) {
+ gSearchPane.buildDefaultEngineDropDowns();
+ }
+ break;
+ }
+ case "engine-default-private": {
+ if (
+ this._separatePrivateDefaultEnabledPref.value &&
+ this._separatePrivateDefaultPref.value
+ ) {
+ // If the user is going through the drop down using up/down keys, the
+ // dropdown may still be open (eg. on Windows) when engine-default is
+ // fired, so rebuilding the list unconditionally would get in the way.
+ const selectedEngine = document.getElementById("defaultPrivateEngine")
+ .selectedItem.engine;
+ if (selectedEngine.name != engine.name) {
+ gSearchPane.buildDefaultEngineDropDowns();
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * nsIObserver implementation.
+ */
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "intl:app-locales-changed": {
+ this.appLocalesChanged();
+ break;
+ }
+ case "browser-search-engine-modified": {
+ this.browserSearchEngineModified(subject, data);
+ break;
+ }
+ }
+ },
+
+ onTreeSelect() {
+ document.getElementById("removeEngineButton").disabled =
+ !gEngineView.isEngineSelectedAndRemovable();
+ },
+
+ onTreeKeyPress(aEvent) {
+ let index = gEngineView.selectedIndex;
+ let tree = document.getElementById("engineList");
+ if (tree.hasAttribute("editing")) {
+ return;
+ }
+
+ if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) {
+ // Space toggles the checkbox.
+ let newValue = !gEngineView.getCellValue(
+ index,
+ tree.columns.getNamedColumn("engineShown")
+ );
+ gEngineView.setCellValue(
+ index,
+ tree.columns.getFirstColumn(),
+ newValue.toString()
+ );
+ // Prevent page from scrolling on the space key.
+ aEvent.preventDefault();
+ } else {
+ let isMac = Services.appinfo.OS == "Darwin";
+ if (
+ (isMac && aEvent.keyCode == KeyEvent.DOM_VK_RETURN) ||
+ (!isMac && aEvent.keyCode == KeyEvent.DOM_VK_F2)
+ ) {
+ this.startEditingAlias(index);
+ } else if (
+ aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (isMac &&
+ aEvent.shiftKey &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE &&
+ gEngineView.isEngineSelectedAndRemovable())
+ ) {
+ // Delete and Shift+Backspace (Mac) removes selected engine.
+ Services.search.removeEngine(gEngineView.selectedEngine.originalEngine);
+ }
+ }
+ },
+
+ startEditingAlias(index) {
+ // Local shortcut aliases can't be edited.
+ if (gEngineView._getLocalShortcut(index)) {
+ return;
+ }
+
+ let tree = document.getElementById("engineList");
+ let engine = gEngineView._engineStore.engines[index];
+ tree.startEditing(index, tree.columns.getLastColumn());
+ tree.inputField.value = engine.alias || "";
+ tree.inputField.select();
+ },
+
+ async onRestoreDefaults() {
+ let num = await gEngineView._engineStore.restoreDefaultEngines();
+ gEngineView.rowCountChanged(0, num);
+ gEngineView.invalidate();
+ },
+
+ showRestoreDefaults(aEnable) {
+ document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable;
+ },
+
+ remove(aEngine) {
+ let index = gEngineView._engineStore.removeEngine(aEngine);
+ if (!gEngineView.tree) {
+ // Only update the selection if it's visible in the UI.
+ return;
+ }
+
+ gEngineView.rowCountChanged(index, -1);
+ gEngineView.invalidate();
+
+ gEngineView.selection.select(Math.min(index, gEngineView.rowCount - 1));
+ gEngineView.ensureRowIsVisible(gEngineView.currentIndex);
+
+ document.getElementById("engineList").focus();
+ },
+
+ async editKeyword(aEngine, aNewKeyword) {
+ let keyword = aNewKeyword.trim();
+ if (keyword) {
+ let eduplicate = false;
+ let dupName = "";
+
+ // Check for duplicates in Places keywords.
+ let bduplicate = !!(await PlacesUtils.keywords.fetch(keyword));
+
+ // Check for duplicates in changes we haven't committed yet
+ let engines = gEngineView._engineStore.engines;
+ let lc_keyword = keyword.toLocaleLowerCase();
+ for (let engine of engines) {
+ if (
+ engine.alias &&
+ engine.alias.toLocaleLowerCase() == lc_keyword &&
+ engine.name != aEngine.name
+ ) {
+ eduplicate = true;
+ dupName = engine.name;
+ break;
+ }
+ }
+
+ // Notify the user if they have chosen an existing engine/bookmark keyword
+ if (eduplicate || bduplicate) {
+ let msgids = [{ id: "search-keyword-warning-title" }];
+ if (eduplicate) {
+ msgids.push({
+ id: "search-keyword-warning-engine",
+ args: { name: dupName },
+ });
+ } else {
+ msgids.push({ id: "search-keyword-warning-bookmark" });
+ }
+
+ let [dtitle, msg] = await document.l10n.formatValues(msgids);
+
+ Services.prompt.alert(window, dtitle, msg);
+ return false;
+ }
+ }
+
+ gEngineView._engineStore.changeEngine(aEngine, "alias", keyword);
+ gEngineView.invalidate();
+ return true;
+ },
+
+ saveOneClickEnginesList() {
+ let hiddenList = [];
+ for (let engine of gEngineView._engineStore.engines) {
+ if (!engine.shown) {
+ hiddenList.push(engine.name);
+ }
+ }
+ Preferences.get("browser.search.hiddenOneOffs").value =
+ hiddenList.join(",");
+ },
+
+ async setDefaultEngine() {
+ await Services.search.setDefault(
+ document.getElementById("defaultEngine").selectedItem.engine,
+ Ci.nsISearchService.CHANGE_REASON_USER
+ );
+ if (ExtensionSettingsStore.getSetting(SEARCH_TYPE, SEARCH_KEY) !== null) {
+ ExtensionSettingsStore.select(
+ ExtensionSettingsStore.SETTING_USER_SET,
+ SEARCH_TYPE,
+ SEARCH_KEY
+ );
+ }
+ },
+
+ async setDefaultPrivateEngine() {
+ await Services.search.setDefaultPrivate(
+ document.getElementById("defaultPrivateEngine").selectedItem.engine,
+ Ci.nsISearchService.CHANGE_REASON_USER
+ );
+ },
+};
+
+function onDragEngineStart(event) {
+ var selectedIndex = gEngineView.selectedIndex;
+
+ // Local shortcut rows can't be dragged or re-ordered.
+ if (gEngineView._getLocalShortcut(selectedIndex)) {
+ event.preventDefault();
+ return;
+ }
+
+ var tree = document.getElementById("engineList");
+ let cell = tree.getCellAt(event.clientX, event.clientY);
+ if (selectedIndex >= 0 && !gEngineView.isCheckBox(cell.row, cell.col)) {
+ event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString());
+ event.dataTransfer.effectAllowed = "move";
+ }
+}
+
+function EngineStore() {
+ let pref = Preferences.get("browser.search.hiddenOneOffs").value;
+ this.hiddenList = pref ? pref.split(",") : [];
+
+ this._engines = [];
+ this._defaultEngines = [];
+ Promise.all([
+ Services.search.getVisibleEngines(),
+ Services.search.getAppProvidedEngines(),
+ ]).then(([visibleEngines, defaultEngines]) => {
+ for (let engine of visibleEngines) {
+ this.addEngine(engine);
+ gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1);
+ }
+ this._defaultEngines = defaultEngines.map(this._cloneEngine, this);
+ gSearchPane.buildDefaultEngineDropDowns();
+
+ // check if we need to disable the restore defaults button
+ var someHidden = this._defaultEngines.some(e => e.hidden);
+ gSearchPane.showRestoreDefaults(someHidden);
+ });
+}
+EngineStore.prototype = {
+ _engines: null,
+ _defaultEngines: null,
+
+ get engines() {
+ return this._engines;
+ },
+ set engines(val) {
+ this._engines = val;
+ },
+
+ _getIndexForEngine(aEngine) {
+ return this._engines.indexOf(aEngine);
+ },
+
+ _getEngineByName(aName) {
+ return this._engines.find(engine => engine.name == aName);
+ },
+
+ _cloneEngine(aEngine) {
+ var clonedObj = {};
+ for (let i of ["id", "name", "alias", "iconURI", "hidden"]) {
+ clonedObj[i] = aEngine[i];
+ }
+ clonedObj.originalEngine = aEngine;
+ clonedObj.shown = !this.hiddenList.includes(clonedObj.name);
+ return clonedObj;
+ },
+
+ // Callback for Array's some(). A thisObj must be passed to some()
+ _isSameEngine(aEngineClone) {
+ return aEngineClone.originalEngine.id == this.originalEngine.id;
+ },
+
+ addEngine(aEngine) {
+ this._engines.push(this._cloneEngine(aEngine));
+ },
+
+ updateEngine(newEngine) {
+ let engineToUpdate = this._engines.findIndex(
+ e => e.originalEngine.id == newEngine.id
+ );
+ if (engineToUpdate != -1) {
+ this.engines[engineToUpdate] = this._cloneEngine(newEngine);
+ }
+ },
+
+ moveEngine(aEngine, aNewIndex) {
+ if (aNewIndex < 0 || aNewIndex > this._engines.length - 1) {
+ throw new Error("ES_moveEngine: invalid aNewIndex!");
+ }
+ var index = this._getIndexForEngine(aEngine);
+ if (index == -1) {
+ throw new Error("ES_moveEngine: invalid engine?");
+ }
+
+ if (index == aNewIndex) {
+ return Promise.resolve();
+ } // nothing to do
+
+ // Move the engine in our internal store
+ var removedEngine = this._engines.splice(index, 1)[0];
+ this._engines.splice(aNewIndex, 0, removedEngine);
+
+ return Services.search.moveEngine(aEngine.originalEngine, aNewIndex);
+ },
+
+ removeEngine(aEngine) {
+ if (this._engines.length == 1) {
+ throw new Error("Cannot remove last engine!");
+ }
+
+ let engineName = aEngine.name;
+ let index = this._engines.findIndex(element => element.name == engineName);
+
+ if (index == -1) {
+ throw new Error("invalid engine?");
+ }
+
+ this._engines.splice(index, 1)[0];
+
+ if (aEngine.isAppProvided) {
+ gSearchPane.showRestoreDefaults(true);
+ }
+ gSearchPane.buildDefaultEngineDropDowns();
+ return index;
+ },
+
+ async restoreDefaultEngines() {
+ var added = 0;
+
+ for (var i = 0; i < this._defaultEngines.length; ++i) {
+ var e = this._defaultEngines[i];
+
+ // If the engine is already in the list, just move it.
+ if (this._engines.some(this._isSameEngine, e)) {
+ await this.moveEngine(this._getEngineByName(e.name), i);
+ } else {
+ // Otherwise, add it back to our internal store
+
+ // The search service removes the alias when an engine is hidden,
+ // so clear any alias we may have cached before unhiding the engine.
+ e.alias = "";
+
+ this._engines.splice(i, 0, e);
+ let engine = e.originalEngine;
+ engine.hidden = false;
+ await Services.search.moveEngine(engine, i);
+ added++;
+ }
+ }
+
+ // We can't do this as part of the loop above because the indices are
+ // used for moving engines.
+ let policyRemovedEngineNames =
+ Services.policies.getActivePolicies()?.SearchEngines?.Remove || [];
+ for (let engineName of policyRemovedEngineNames) {
+ let engine = Services.search.getEngineByName(engineName);
+ if (engine) {
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {
+ // Engine might not exist
+ }
+ }
+ }
+
+ Services.search.resetToAppDefaultEngine();
+ gSearchPane.showRestoreDefaults(false);
+ gSearchPane.buildDefaultEngineDropDowns();
+ return added;
+ },
+
+ changeEngine(aEngine, aProp, aNewValue) {
+ var index = this._getIndexForEngine(aEngine);
+ if (index == -1) {
+ throw new Error("invalid engine?");
+ }
+
+ this._engines[index][aProp] = aNewValue;
+ aEngine.originalEngine[aProp] = aNewValue;
+ },
+
+ reloadIcons() {
+ this._engines.forEach(function (e) {
+ e.iconURI = e.originalEngine.iconURI;
+ });
+ },
+};
+
+function EngineView(aEngineStore) {
+ this._engineStore = aEngineStore;
+
+ UrlbarPrefs.addObserver(this);
+
+ this.loadL10nNames();
+}
+
+EngineView.prototype = {
+ _engineStore: null,
+ tree: null,
+
+ loadL10nNames() {
+ // This maps local shortcut sources to their l10n names. The names are needed
+ // by getCellText. Getting the names is async but getCellText is not, so we
+ // cache them here to retrieve them syncronously in getCellText.
+ this._localShortcutL10nNames = new Map();
+ return document.l10n
+ .formatValues(
+ UrlbarUtils.LOCAL_SEARCH_MODES.map(mode => {
+ let name = UrlbarUtils.getResultSourceName(mode.source);
+ return { id: `urlbar-search-mode-${name}` };
+ })
+ )
+ .then(names => {
+ for (let { source } of UrlbarUtils.LOCAL_SEARCH_MODES) {
+ this._localShortcutL10nNames.set(source, names.shift());
+ }
+ // Invalidate the tree now that we have the names in case getCellText was
+ // called before name retrieval finished.
+ this.invalidate();
+ });
+ },
+
+ get lastEngineIndex() {
+ return this._engineStore.engines.length - 1;
+ },
+
+ get selectedIndex() {
+ var seln = this.selection;
+ if (seln.getRangeCount() > 0) {
+ var min = {};
+ seln.getRangeAt(0, min, {});
+ return min.value;
+ }
+ return -1;
+ },
+
+ get selectedEngine() {
+ return this._engineStore.engines[this.selectedIndex];
+ },
+
+ // Helpers
+ rowCountChanged(index, count) {
+ if (this.tree) {
+ this.tree.rowCountChanged(index, count);
+ }
+ },
+
+ invalidate() {
+ this.tree?.invalidate();
+ },
+
+ ensureRowIsVisible(index) {
+ this.tree.ensureRowIsVisible(index);
+ },
+
+ getSourceIndexFromDrag(dataTransfer) {
+ return parseInt(dataTransfer.getData(ENGINE_FLAVOR));
+ },
+
+ isCheckBox(index, column) {
+ return column.id == "engineShown";
+ },
+
+ isEngineSelectedAndRemovable() {
+ let defaultEngine = Services.search.defaultEngine;
+ let defaultPrivateEngine = Services.search.defaultPrivateEngine;
+ // We don't allow the last remaining engine to be removed, thus the
+ // `this.lastEngineIndex != 0` check.
+ // We don't allow the default engine to be removed.
+ return (
+ this.selectedIndex != -1 &&
+ this.lastEngineIndex != 0 &&
+ !this._getLocalShortcut(this.selectedIndex) &&
+ this.selectedEngine.name != defaultEngine.name &&
+ this.selectedEngine.name != defaultPrivateEngine.name
+ );
+ },
+
+ /**
+ * Returns the local shortcut corresponding to a tree row, or null if the row
+ * is not a local shortcut.
+ *
+ * @param {number} index
+ * The tree row index.
+ * @returns {object}
+ * The local shortcut object or null if the row is not a local shortcut.
+ */
+ _getLocalShortcut(index) {
+ let engineCount = this._engineStore.engines.length;
+ if (index < engineCount) {
+ return null;
+ }
+ return UrlbarUtils.LOCAL_SEARCH_MODES[index - engineCount];
+ },
+
+ /**
+ * Called by UrlbarPrefs when a urlbar pref changes.
+ *
+ * @param {string} pref
+ * The name of the pref relative to the browser.urlbar branch.
+ */
+ onPrefChanged(pref) {
+ // If one of the local shortcut prefs was toggled, toggle its row's
+ // checkbox.
+ let parts = pref.split(".");
+ if (parts[0] == "shortcuts" && parts[1] && parts.length == 2) {
+ this.invalidate();
+ }
+ },
+
+ // nsITreeView
+ get rowCount() {
+ return (
+ this._engineStore.engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length
+ );
+ },
+
+ getImageSrc(index, column) {
+ if (column.id == "engineName") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return shortcut.icon;
+ }
+
+ if (this._engineStore.engines[index].iconURI) {
+ return this._engineStore.engines[index].iconURI.spec;
+ }
+
+ if (window.devicePixelRatio > 1) {
+ return "chrome://browser/skin/search-engine-placeholder@2x.png";
+ }
+ return "chrome://browser/skin/search-engine-placeholder.png";
+ }
+
+ return "";
+ },
+
+ getCellText(index, column) {
+ if (column.id == "engineName") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return this._localShortcutL10nNames.get(shortcut.source) || "";
+ }
+ return this._engineStore.engines[index].name;
+ } else if (column.id == "engineKeyword") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return shortcut.restrict;
+ }
+ return this._engineStore.engines[index].originalEngine.aliases.join(", ");
+ }
+ return "";
+ },
+
+ setTree(tree) {
+ this.tree = tree;
+ },
+
+ canDrop(targetIndex, orientation, dataTransfer) {
+ var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
+ return (
+ sourceIndex != -1 &&
+ sourceIndex != targetIndex &&
+ sourceIndex != targetIndex + orientation &&
+ // Local shortcut rows can't be dragged or dropped on.
+ targetIndex < this._engineStore.engines.length
+ );
+ },
+
+ async drop(dropIndex, orientation, dataTransfer) {
+ // Local shortcut rows can't be dragged or dropped on. This can sometimes
+ // be reached even though canDrop returns false for these rows.
+ if (this._engineStore.engines.length <= dropIndex) {
+ return;
+ }
+
+ var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
+ var sourceEngine = this._engineStore.engines[sourceIndex];
+
+ const nsITreeView = Ci.nsITreeView;
+ if (dropIndex > sourceIndex) {
+ if (orientation == nsITreeView.DROP_BEFORE) {
+ dropIndex--;
+ }
+ } else if (orientation == nsITreeView.DROP_AFTER) {
+ dropIndex++;
+ }
+
+ await this._engineStore.moveEngine(sourceEngine, dropIndex);
+ gSearchPane.showRestoreDefaults(true);
+ gSearchPane.buildDefaultEngineDropDowns();
+
+ // Redraw, and adjust selection
+ this.invalidate();
+ this.selection.select(dropIndex);
+ },
+
+ selection: null,
+ getRowProperties(index) {
+ return "";
+ },
+ getCellProperties(index, column) {
+ if (column.id == "engineName") {
+ // For local shortcut rows, return the result source name so we can style
+ // the icons in CSS.
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return UrlbarUtils.getResultSourceName(shortcut.source);
+ }
+ }
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ isContainer(index) {
+ return false;
+ },
+ isContainerOpen(index) {
+ return false;
+ },
+ isContainerEmpty(index) {
+ return false;
+ },
+ isSeparator(index) {
+ return false;
+ },
+ isSorted(index) {
+ return false;
+ },
+ getParentIndex(index) {
+ return -1;
+ },
+ hasNextSibling(parentIndex, index) {
+ return false;
+ },
+ getLevel(index) {
+ return 0;
+ },
+ getCellValue(index, column) {
+ if (column.id == "engineShown") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return UrlbarPrefs.get(shortcut.pref);
+ }
+ return this._engineStore.engines[index].shown;
+ }
+ return undefined;
+ },
+ toggleOpenState(index) {},
+ cycleHeader(column) {},
+ selectionChanged() {},
+ cycleCell(row, column) {},
+ isEditable(index, column) {
+ return (
+ column.id != "engineName" &&
+ (column.id == "engineShown" || !this._getLocalShortcut(index))
+ );
+ },
+ setCellValue(index, column, value) {
+ if (column.id == "engineShown") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ UrlbarPrefs.set(shortcut.pref, value == "true");
+ this.invalidate();
+ return;
+ }
+ this._engineStore.engines[index].shown = value == "true";
+ gEngineView.invalidate();
+ gSearchPane.saveOneClickEnginesList();
+ }
+ },
+ setCellText(index, column, value) {
+ if (column.id == "engineKeyword") {
+ gSearchPane
+ .editKeyword(this._engineStore.engines[index], value)
+ .then(valid => {
+ if (!valid) {
+ gSearchPane.startEditingAlias(index);
+ }
+ });
+ }
+ },
+};
diff --git a/browser/components/preferences/searchResults.inc.xhtml b/browser/components/preferences/searchResults.inc.xhtml
new file mode 100644
index 0000000000..48dbeb9a54
--- /dev/null
+++ b/browser/components/preferences/searchResults.inc.xhtml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<hbox id="header-searchResults"
+ class="subcategory"
+ hidden="true"
+ data-hidden-from-search="true"
+ data-category="paneSearchResults">
+ <html:h1 data-l10n-id="search-results-header"/>
+</hbox>
+
+<groupbox id="no-results-message"
+ data-hidden-from-search="true"
+ data-category="paneSearchResults"
+ hidden="true">
+ <vbox class="no-results-container">
+ <label id="sorry-message" data-l10n-id="search-results-empty-message2">
+ <html:span data-l10n-name="query" id="sorry-message-query"/>
+ </label>
+ <label id="need-help" data-l10n-id="search-results-help-link">
+ <html:a is="moz-support-link" class="text-link" data-l10n-name="url" target="_blank" support-page="preferences"/>
+ </label>
+ </vbox>
+</groupbox>
diff --git a/browser/components/preferences/sync.inc.xhtml b/browser/components/preferences/sync.inc.xhtml
new file mode 100644
index 0000000000..7df27eb994
--- /dev/null
+++ b/browser/components/preferences/sync.inc.xhtml
@@ -0,0 +1,245 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<!-- Sync panel -->
+
+<script src="chrome://browser/content/preferences/sync.js"/>
+<html:template id="template-paneSync">
+<hbox id="firefoxAccountCategory"
+ class="subcategory"
+ hidden="true"
+ data-category="paneSync">
+ <html:h1 data-l10n-id="pane-sync-title3"/>
+</hbox>
+
+<deck id="weavePrefsDeck" data-category="paneSync" hidden="true"
+ data-hidden-from-search="true">
+ <groupbox id="noFxaAccount">
+ <hbox>
+ <vbox flex="1">
+ <label id="noFxaCaption"><html:h2 data-l10n-id="sync-signedout-caption"/></label>
+ <description id="noFxaDescription" flex="1" data-l10n-id="sync-signedout-description2"/>
+ </vbox>
+ <vbox>
+ <image class="fxaSyncIllustration"/>
+ </vbox>
+ </hbox>
+ <hbox id="fxaNoLoginStatus" align="center" flex="1">
+ <vbox flex="1">
+ <hbox align="center" flex="1">
+ <button id="noFxaSignIn"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="sync-signedout-account-signin3"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ <label class="fxaMobilePromo" data-l10n-id="sync-mobile-promo">
+ <html:img
+ src="chrome://browser/skin/logo-android.svg"
+ data-l10n-name="android-icon"
+ class="androidIcon"/>
+ <html:a
+ data-l10n-name="android-link"
+ class="fxaMobilePromo-android text-link" target="_blank"/>
+ <html:img
+ src="chrome://browser/skin/logo-ios.svg"
+ data-l10n-name="ios-icon"
+ class="iOSIcon"/>
+ <html:a
+ data-l10n-name="ios-link"
+ class="fxaMobilePromo-ios text-link" target="_blank"/>
+ </label>
+ </groupbox>
+
+ <vbox id="hasFxaAccount">
+ <hbox>
+ <vbox id="fxaContentWrapper" flex="1">
+ <groupbox id="fxaGroup">
+ <label class="search-header" hidden="true"><html:h2 data-l10n-id="pane-sync-title3"/></label>
+
+ <deck id="fxaLoginStatus" flex="1">
+
+ <!-- logged in and verified and all is good -->
+ <hbox id="fxaLoginVerified" align="center" flex="1">
+ <image id="openChangeProfileImage"
+ class="fxaProfileImage actionable"
+ role="button"
+ data-l10n-id="sync-profile-picture"/>
+ <vbox flex="1" pack="center">
+ <hbox flex="1" align="baseline">
+ <label id="fxaDisplayName" hidden="true">
+ <html:h2 id="fxaDisplayNameHeading"/>
+ </label>
+ <label id="fxaEmailAddress" flex="1" crop="end"/>
+ <button id="fxaUnlinkButton"
+ is="highlightable-button"
+ class="accessory-button"
+ data-l10n-id="sync-sign-out"/>
+ </hbox>
+ <hbox>
+ <html:a id="verifiedManage" class="openLink" target="_blank"
+ data-l10n-id="sync-manage-account"/>
+ </hbox>
+ </vbox>
+ </hbox>
+
+ <!-- logged in to an unverified account -->
+ <hbox id="fxaLoginUnverified">
+ <vbox>
+ <image class="fxaProfileImage"/>
+ </vbox>
+ <vbox flex="1" pack="center">
+ <hbox align="center">
+ <image class="fxaLoginRejectedWarning"/>
+ <description flex="1"
+ class="l10nArgsEmailAddress"
+ data-l10n-id="sync-signedin-unverified"
+ data-l10n-args='{"email": ""}'/>
+ </hbox>
+ <hbox class="fxaAccountBoxButtons">
+ <button id="verifyFxaAccount"
+ is="highlightable-button"
+ data-l10n-id="sync-resend-verification"/>
+ <button id="unverifiedUnlinkFxaAccount"
+ is="highlightable-button"
+ data-l10n-id="sync-remove-account"/>
+ </hbox>
+ </vbox>
+ </hbox>
+
+ <!-- logged in locally but server rejected credentials -->
+ <hbox id="fxaLoginRejected">
+ <vbox>
+ <image class="fxaProfileImage"/>
+ </vbox>
+ <vbox flex="1" pack="center">
+ <hbox align="center">
+ <image class="fxaLoginRejectedWarning"/>
+ <description flex="1"
+ class="l10nArgsEmailAddress"
+ data-l10n-id="sync-signedin-login-failure"
+ data-l10n-args='{"email": ""}'/>
+ </hbox>
+ <hbox class="fxaAccountBoxButtons">
+ <button id="rejectReSignIn"
+ is="highlightable-button"
+ data-l10n-id="sync-sign-in"/>
+ <button id="rejectUnlinkFxaAccount"
+ is="highlightable-button"
+ data-l10n-id="sync-remove-account"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ </deck>
+ </groupbox>
+
+ <groupbox>
+ <label control="fxaSyncComputerName">
+ <html:h2 data-l10n-id="sync-device-name-header"/>
+ </label>
+ <hbox id="fxaDeviceName">
+ <html:input id="fxaSyncComputerName" type="text" disabled="true"/>
+ <button id="fxaChangeDeviceName"
+ is="highlightable-button"
+ class="needs-account-ready"
+ data-l10n-id="sync-device-name-change"/>
+ <button id="fxaCancelChangeDeviceName"
+ is="highlightable-button"
+ data-l10n-id="sync-device-name-cancel"
+ hidden="true"/>
+ <button id="fxaSaveChangeDeviceName"
+ is="highlightable-button"
+ data-l10n-id="sync-device-name-save"
+ hidden="true"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <deck id="syncStatus" flex="1">
+ <!-- sync not yet configured. -->
+ <vbox id="syncNotConfigured">
+ <label>
+ <html:h2 data-l10n-id="prefs-syncing-off"/>
+ </label>
+ <hbox class="sync-group info-box-container">
+ <vbox flex="1">
+ <label data-l10n-id="prefs-sync-offer-setup-label2"/>
+ </vbox>
+ <vbox>
+ <button id="syncSetup"
+ is="highlightable-button"
+ class="accessory-button needs-account-ready"
+ data-l10n-id="prefs-sync-turn-on-syncing"/>
+ </vbox>
+ </hbox>
+ </vbox>
+
+ <vbox id="syncConfigured">
+ <hbox>
+ <html:h2 data-l10n-id="prefs-syncing-on"/>
+ <spacer flex="1"/>
+ <button id="syncNow"
+ class="accessory-button needs-account-ready"
+ data-l10n-id="prefs-sync-now"
+ data-l10n-attrs="labelnotsyncing, accesskeynotsyncing, labelsyncing"/>
+ </hbox>
+ <vbox class="sync-group info-box-container sync-configured">
+ <label data-l10n-id="sync-syncing-across-devices-heading"/>
+ <html:div class="sync-engines-list">
+ <html:div engine_preference="services.sync.engine.bookmarks">
+ <image class="sync-engine-image sync-engine-bookmarks"/>
+ <label data-l10n-id="sync-currently-syncing-bookmarks"/>
+ </html:div>
+ <html:div engine_preference="services.sync.engine.history">
+ <image class="sync-engine-image sync-engine-history"/>
+ <label data-l10n-id="sync-currently-syncing-history"/>
+ </html:div>
+ <html:div engine_preference="services.sync.engine.tabs">
+ <image class="sync-engine-image sync-engine-tabs"/>
+ <label data-l10n-id="sync-currently-syncing-tabs"/>
+ </html:div>
+ <html:div engine_preference="services.sync.engine.passwords">
+ <image class="sync-engine-image sync-engine-passwords"/>
+ <label data-l10n-id="sync-currently-syncing-logins-passwords"/>
+ </html:div>
+ <html:div engine_preference="services.sync.engine.addresses">
+ <image class="sync-engine-image sync-engine-addresses"/>
+ <label data-l10n-id="sync-currently-syncing-addresses"/>
+ </html:div>
+ <html:div engine_preference="services.sync.engine.creditcards">
+ <image class="sync-engine-image sync-engine-creditcards"/>
+ <label data-l10n-id="sync-currently-syncing-creditcards"/>
+ </html:div>
+ <html:div engine_preference="services.sync.engine.addons">
+ <image class="sync-engine-image sync-engine-addons"/>
+ <label data-l10n-id="sync-currently-syncing-addons"/>
+ </html:div>
+ <html:div engine_preference="services.sync.engine.prefs">
+ <image class="sync-engine-image sync-engine-prefs"/>
+ <label data-l10n-id="sync-currently-syncing-settings"/>
+ </html:div>
+ </html:div>
+ <hbox>
+ <button id="syncChangeOptions"
+ is="highlightable-button"
+ data-l10n-id="sync-change-options"/>
+ <spacer flex="1"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </deck>
+ </groupbox>
+ </vbox>
+ </hbox>
+ <vbox align="start">
+ <html:a id="connect-another-device"
+ is="text-link"
+ class="fxaMobilePromo"
+ target="_blank"
+ data-l10n-id="sync-connect-another-device"/>
+ </vbox>
+ </vbox>
+</deck>
+</html:template>
diff --git a/browser/components/preferences/sync.js b/browser/components/preferences/sync.js
new file mode 100644
index 0000000000..80e89fd1c2
--- /dev/null
+++ b/browser/components/preferences/sync.js
@@ -0,0 +1,564 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from preferences.js */
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
+ return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
+});
+
+const FXA_PAGE_LOGGED_OUT = 0;
+const FXA_PAGE_LOGGED_IN = 1;
+
+// Indexes into the "login status" deck.
+// We are in a successful verified state - everything should work!
+const FXA_LOGIN_VERIFIED = 0;
+// We have logged in to an unverified account.
+const FXA_LOGIN_UNVERIFIED = 1;
+// We are logged in locally, but the server rejected our credentials.
+const FXA_LOGIN_FAILED = 2;
+
+// Indexes into the "sync status" deck.
+const SYNC_DISCONNECTED = 0;
+const SYNC_CONNECTED = 1;
+
+var gSyncPane = {
+ get page() {
+ return document.getElementById("weavePrefsDeck").selectedIndex;
+ },
+
+ set page(val) {
+ document.getElementById("weavePrefsDeck").selectedIndex = val;
+ },
+
+ init() {
+ this._setupEventListeners();
+ this.setupEnginesUI();
+
+ document
+ .getElementById("weavePrefsDeck")
+ .removeAttribute("data-hidden-from-search");
+
+ // If the Service hasn't finished initializing, wait for it.
+ let xps = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+
+ if (xps.ready) {
+ this._init();
+ return;
+ }
+
+ // it may take some time before all the promises we care about resolve, so
+ // pre-load what we can from synchronous sources.
+ this._showLoadPage(xps);
+
+ let onUnload = function () {
+ window.removeEventListener("unload", onUnload);
+ try {
+ Services.obs.removeObserver(onReady, "weave:service:ready");
+ } catch (e) {}
+ };
+
+ let onReady = () => {
+ Services.obs.removeObserver(onReady, "weave:service:ready");
+ window.removeEventListener("unload", onUnload);
+ this._init();
+ };
+
+ Services.obs.addObserver(onReady, "weave:service:ready");
+ window.addEventListener("unload", onUnload);
+
+ xps.ensureLoaded();
+ },
+
+ _showLoadPage(xps) {
+ let maybeAcct = false;
+ let username = Services.prefs.getCharPref("services.sync.username", "");
+ if (username) {
+ document.getElementById("fxaEmailAddress").textContent = username;
+ maybeAcct = true;
+ }
+
+ let cachedComputerName = Services.prefs.getStringPref(
+ "identity.fxaccounts.account.device.name",
+ ""
+ );
+ if (cachedComputerName) {
+ maybeAcct = true;
+ this._populateComputerName(cachedComputerName);
+ }
+ this.page = maybeAcct ? FXA_PAGE_LOGGED_IN : FXA_PAGE_LOGGED_OUT;
+ },
+
+ _init() {
+ Weave.Svc.Obs.add(UIState.ON_UPDATE, this.updateWeavePrefs, this);
+
+ window.addEventListener("unload", () => {
+ Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.updateWeavePrefs, this);
+ });
+
+ FxAccounts.config
+ .promiseConnectDeviceURI(this._getEntryPoint())
+ .then(connectURI => {
+ document
+ .getElementById("connect-another-device")
+ .setAttribute("href", connectURI);
+ });
+ // Links for mobile devices.
+ for (let platform of ["android", "ios"]) {
+ let url =
+ Services.prefs.getCharPref(`identity.mobilepromo.${platform}`) +
+ "sync-preferences";
+ for (let elt of document.querySelectorAll(
+ `.fxaMobilePromo-${platform}`
+ )) {
+ elt.setAttribute("href", url);
+ }
+ }
+
+ this.updateWeavePrefs();
+
+ // Notify observers that the UI is now ready
+ Services.obs.notifyObservers(window, "sync-pane-loaded");
+
+ if (
+ location.hash == "#sync" &&
+ UIState.get().status == UIState.STATUS_SIGNED_IN
+ ) {
+ if (location.href.includes("action=pair")) {
+ gSyncPane.pairAnotherDevice();
+ } else if (location.href.includes("action=choose-what-to-sync")) {
+ gSyncPane._chooseWhatToSync(false);
+ }
+ }
+ },
+
+ _toggleComputerNameControls(editMode) {
+ let textbox = document.getElementById("fxaSyncComputerName");
+ textbox.disabled = !editMode;
+ document.getElementById("fxaChangeDeviceName").hidden = editMode;
+ document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode;
+ document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode;
+ },
+
+ _focusComputerNameTextbox() {
+ let textbox = document.getElementById("fxaSyncComputerName");
+ let valLength = textbox.value.length;
+ textbox.focus();
+ textbox.setSelectionRange(valLength, valLength);
+ },
+
+ _blurComputerNameTextbox() {
+ document.getElementById("fxaSyncComputerName").blur();
+ },
+
+ _focusAfterComputerNameTextbox() {
+ // Focus the most appropriate element that's *not* the "computer name" box.
+ Services.focus.moveFocus(
+ window,
+ document.getElementById("fxaSyncComputerName"),
+ Services.focus.MOVEFOCUS_FORWARD,
+ 0
+ );
+ },
+
+ _updateComputerNameValue(save) {
+ if (save) {
+ let textbox = document.getElementById("fxaSyncComputerName");
+ Weave.Service.clientsEngine.localName = textbox.value;
+ }
+ this._populateComputerName(Weave.Service.clientsEngine.localName);
+ },
+
+ _setupEventListeners() {
+ function setEventListener(aId, aEventType, aCallback) {
+ document
+ .getElementById(aId)
+ .addEventListener(aEventType, aCallback.bind(gSyncPane));
+ }
+
+ setEventListener("openChangeProfileImage", "click", function (event) {
+ gSyncPane.openChangeProfileImage(event);
+ });
+ setEventListener("openChangeProfileImage", "keypress", function (event) {
+ gSyncPane.openChangeProfileImage(event);
+ });
+ setEventListener("fxaChangeDeviceName", "command", function () {
+ this._toggleComputerNameControls(true);
+ this._focusComputerNameTextbox();
+ });
+ setEventListener("fxaCancelChangeDeviceName", "command", function () {
+ // We explicitly blur the textbox because of bug 75324, then after
+ // changing the state of the buttons, force focus to whatever the focus
+ // manager thinks should be next (which on the mac, depends on an OSX
+ // keyboard access preference)
+ this._blurComputerNameTextbox();
+ this._toggleComputerNameControls(false);
+ this._updateComputerNameValue(false);
+ this._focusAfterComputerNameTextbox();
+ });
+ setEventListener("fxaSaveChangeDeviceName", "command", function () {
+ // Work around bug 75324 - see above.
+ this._blurComputerNameTextbox();
+ this._toggleComputerNameControls(false);
+ this._updateComputerNameValue(true);
+ this._focusAfterComputerNameTextbox();
+ });
+ setEventListener("noFxaSignIn", "command", function () {
+ gSyncPane.signIn();
+ return false;
+ });
+ setEventListener("fxaUnlinkButton", "command", function () {
+ gSyncPane.unlinkFirefoxAccount(true);
+ });
+ setEventListener(
+ "verifyFxaAccount",
+ "command",
+ gSyncPane.verifyFirefoxAccount
+ );
+ setEventListener("unverifiedUnlinkFxaAccount", "command", function () {
+ /* no warning as account can't have previously synced */
+ gSyncPane.unlinkFirefoxAccount(false);
+ });
+ setEventListener("rejectReSignIn", "command", gSyncPane.reSignIn);
+ setEventListener("rejectUnlinkFxaAccount", "command", function () {
+ gSyncPane.unlinkFirefoxAccount(true);
+ });
+ setEventListener("fxaSyncComputerName", "keypress", function (e) {
+ if (e.keyCode == KeyEvent.DOM_VK_RETURN) {
+ document.getElementById("fxaSaveChangeDeviceName").click();
+ } else if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ document.getElementById("fxaCancelChangeDeviceName").click();
+ }
+ });
+ setEventListener("syncSetup", "command", function () {
+ this._chooseWhatToSync(false);
+ });
+ setEventListener("syncChangeOptions", "command", function () {
+ this._chooseWhatToSync(true);
+ });
+ setEventListener("syncNow", "command", function () {
+ // syncing can take a little time to send the "started" notification, so
+ // pretend we already got it.
+ this._updateSyncNow(true);
+ Weave.Service.sync({ why: "aboutprefs" });
+ });
+ setEventListener("syncNow", "mouseover", function () {
+ const state = UIState.get();
+ // If we are currently syncing, just set the tooltip to the same as the
+ // button label (ie, "Syncing...")
+ let tooltiptext = state.syncing
+ ? document.getElementById("syncNow").getAttribute("label")
+ : window.browsingContext.topChromeWindow.gSync.formatLastSyncDate(
+ state.lastSync
+ );
+ document
+ .getElementById("syncNow")
+ .setAttribute("tooltiptext", tooltiptext);
+ });
+ },
+
+ async _chooseWhatToSync(isAlreadySyncing) {
+ // Assuming another device is syncing and we're not,
+ // we update the engines selection so the correct
+ // checkboxes are pre-filed.
+ if (!isAlreadySyncing) {
+ try {
+ await Weave.Service.updateLocalEnginesState();
+ } catch (err) {
+ console.error("Error updating the local engines state", err);
+ }
+ }
+ let params = {};
+ if (isAlreadySyncing) {
+ // If we are already syncing then we also offer to disconnect.
+ params.disconnectFun = () => this.disconnectSync();
+ }
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml",
+ {
+ closingCallback: event => {
+ if (!isAlreadySyncing && event.detail.button == "accept") {
+ // We weren't syncing but the user has accepted the dialog - so we
+ // want to start!
+ fxAccounts.telemetry
+ .recordConnection(["sync"], "ui")
+ .then(() => {
+ return Weave.Service.configure();
+ })
+ .catch(err => {
+ console.error("Failed to enable sync", err);
+ });
+ }
+ },
+ },
+ params /* aParams */
+ );
+ },
+
+ _updateSyncNow(syncing) {
+ let butSyncNow = document.getElementById("syncNow");
+ if (syncing) {
+ butSyncNow.setAttribute("label", butSyncNow.getAttribute("labelsyncing"));
+ butSyncNow.removeAttribute("accesskey");
+ butSyncNow.disabled = true;
+ } else {
+ butSyncNow.setAttribute(
+ "label",
+ butSyncNow.getAttribute("labelnotsyncing")
+ );
+ butSyncNow.setAttribute(
+ "accesskey",
+ butSyncNow.getAttribute("accesskeynotsyncing")
+ );
+ butSyncNow.disabled = false;
+ }
+ },
+
+ updateWeavePrefs() {
+ let service = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+
+ let displayNameLabel = document.getElementById("fxaDisplayName");
+ let fxaEmailAddressLabels = document.querySelectorAll(
+ ".l10nArgsEmailAddress"
+ );
+ displayNameLabel.hidden = true;
+
+ // while we determine the fxa status pre-load what we can.
+ this._showLoadPage(service);
+
+ let state = UIState.get();
+ if (state.status == UIState.STATUS_NOT_CONFIGURED) {
+ this.page = FXA_PAGE_LOGGED_OUT;
+ return;
+ }
+ this.page = FXA_PAGE_LOGGED_IN;
+ // We are logged in locally, but maybe we are in a state where the
+ // server rejected our credentials (eg, password changed on the server)
+ let fxaLoginStatus = document.getElementById("fxaLoginStatus");
+ let syncReady = false; // Is sync able to actually sync?
+ // We need to check error states that need a re-authenticate to resolve
+ // themselves first.
+ if (state.status == UIState.STATUS_LOGIN_FAILED) {
+ fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED;
+ } else if (state.status == UIState.STATUS_NOT_VERIFIED) {
+ fxaLoginStatus.selectedIndex = FXA_LOGIN_UNVERIFIED;
+ } else {
+ // We must be golden (or in an error state we expect to magically
+ // resolve itself)
+ fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED;
+ syncReady = true;
+ }
+ fxaEmailAddressLabels.forEach(label => {
+ let l10nAttrs = document.l10n.getAttributes(label);
+ document.l10n.setAttributes(label, l10nAttrs.id, { email: state.email });
+ });
+ document.getElementById("fxaEmailAddress").textContent = state.email;
+
+ this._populateComputerName(Weave.Service.clientsEngine.localName);
+ for (let elt of document.querySelectorAll(".needs-account-ready")) {
+ elt.disabled = !syncReady;
+ }
+
+ // Clear the profile image (if any) of the previously logged in account.
+ document
+ .querySelector("#fxaLoginVerified > .fxaProfileImage")
+ .style.removeProperty("list-style-image");
+
+ if (state.displayName) {
+ fxaLoginStatus.setAttribute("hasName", true);
+ displayNameLabel.hidden = false;
+ document.getElementById("fxaDisplayNameHeading").textContent =
+ state.displayName;
+ } else {
+ fxaLoginStatus.removeAttribute("hasName");
+ }
+ if (state.avatarURL && !state.avatarIsDefault) {
+ let bgImage = 'url("' + state.avatarURL + '")';
+ let profileImageElement = document.querySelector(
+ "#fxaLoginVerified > .fxaProfileImage"
+ );
+ profileImageElement.style.listStyleImage = bgImage;
+
+ let img = new Image();
+ img.onerror = () => {
+ // Clear the image if it has trouble loading. Since this callback is asynchronous
+ // we check to make sure the image is still the same before we clear it.
+ if (profileImageElement.style.listStyleImage === bgImage) {
+ profileImageElement.style.removeProperty("list-style-image");
+ }
+ };
+ img.src = state.avatarURL;
+ }
+ // The "manage account" link embeds the uid, so we need to update this
+ // if the account state changes.
+ FxAccounts.config
+ .promiseManageURI(this._getEntryPoint())
+ .then(accountsManageURI => {
+ document
+ .getElementById("verifiedManage")
+ .setAttribute("href", accountsManageURI);
+ });
+ // and the actual sync state.
+ let eltSyncStatus = document.getElementById("syncStatus");
+ eltSyncStatus.hidden = !syncReady;
+ eltSyncStatus.selectedIndex = state.syncEnabled
+ ? SYNC_CONNECTED
+ : SYNC_DISCONNECTED;
+ this._updateSyncNow(state.syncing);
+ },
+
+ _getEntryPoint() {
+ let params = new URLSearchParams(
+ document.URL.split("#")[0].split("?")[1] || ""
+ );
+ return params.get("entrypoint") || "preferences";
+ },
+
+ openContentInBrowser(url, options) {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!win) {
+ openTrustedLinkIn(url, "tab");
+ return;
+ }
+ win.switchToTabHavingURI(url, true, options);
+ },
+
+ // Replace the current tab with the specified URL.
+ replaceTabWithUrl(url) {
+ // Get the <browser> element hosting us.
+ let browser = window.docShell.chromeEventHandler;
+ // And tell it to load our URL.
+ browser.loadURI(Services.io.newURI(url), {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ });
+ },
+
+ async signIn() {
+ if (!(await FxAccounts.canConnectAccount())) {
+ return;
+ }
+ const url = await FxAccounts.config.promiseConnectAccountURI(
+ this._getEntryPoint()
+ );
+ this.replaceTabWithUrl(url);
+ },
+
+ async reSignIn() {
+ // There's a bit of an edge-case here - we might be forcing reauth when we've
+ // lost the FxA account data - in which case we'll not get a URL as the re-auth
+ // URL embeds account info and the server endpoint complains if we don't
+ // supply it - So we just use the regular "sign in" URL in that case.
+ if (!(await FxAccounts.canConnectAccount())) {
+ return;
+ }
+
+ let entryPoint = this._getEntryPoint();
+ const url =
+ (await FxAccounts.config.promiseForceSigninURI(entryPoint)) ||
+ (await FxAccounts.config.promiseConnectAccountURI(entryPoint));
+ this.replaceTabWithUrl(url);
+ },
+
+ clickOrSpaceOrEnterPressed(event) {
+ // Note: charCode is deprecated, but 'char' not yet implemented.
+ // Replace charCode with char when implemented, see Bug 680830
+ return (
+ (event.type == "click" && event.button == 0) ||
+ (event.type == "keypress" &&
+ (event.charCode == KeyEvent.DOM_VK_SPACE ||
+ event.keyCode == KeyEvent.DOM_VK_RETURN))
+ );
+ },
+
+ openChangeProfileImage(event) {
+ if (this.clickOrSpaceOrEnterPressed(event)) {
+ FxAccounts.config
+ .promiseChangeAvatarURI(this._getEntryPoint())
+ .then(url => {
+ this.openContentInBrowser(url, {
+ replaceQueryString: true,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ });
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ }
+ },
+
+ async verifyFirefoxAccount() {
+ let titleL10nid, bodyL10nId;
+ try {
+ await fxAccounts.resendVerificationEmail();
+ const { email } = await fxAccounts.getSignedInUser();
+ titleL10nid = "sync-verification-sent-title";
+ bodyL10nId = { id: "sync-verification-sent-body", args: { email } };
+ } catch {
+ titleL10nid = "sync-verification-not-sent-title";
+ bodyL10nId = "sync-verification-not-sent-body";
+ }
+ const [title, body] = await document.l10n.formatValues([
+ titleL10nid,
+ bodyL10nId,
+ ]);
+ new Notification(title, { body });
+ },
+
+ // Disconnect the account, including everything linked.
+ unlinkFirefoxAccount(confirm) {
+ window.browsingContext.topChromeWindow.gSync.disconnect({
+ confirm,
+ });
+ },
+
+ // Disconnect sync, leaving the account connected.
+ disconnectSync() {
+ return window.browsingContext.topChromeWindow.gSync.disconnect({
+ confirm: true,
+ disconnectAccount: false,
+ });
+ },
+
+ pairAnotherDevice() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/fxaPairDevice.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ _populateComputerName(value) {
+ let textbox = document.getElementById("fxaSyncComputerName");
+ if (!textbox.hasAttribute("placeholder")) {
+ textbox.setAttribute(
+ "placeholder",
+ fxAccounts.device.getDefaultLocalName()
+ );
+ }
+ textbox.value = value;
+ },
+
+ // arranges to dynamically show or hide sync engine name elements based on the
+ // preferences used for this engines.
+ setupEnginesUI() {
+ let observe = (elt, prefName) => {
+ elt.hidden = !Services.prefs.getBoolPref(prefName, false);
+ };
+
+ for (let elt of document.querySelectorAll("[engine_preference]")) {
+ let prefName = elt.getAttribute("engine_preference");
+ let obs = observe.bind(null, elt, prefName);
+ obs();
+ Services.prefs.addObserver(prefName, obs);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver(prefName, obs);
+ });
+ }
+ },
+};
diff --git a/browser/components/preferences/tests/addons/pl-dictionary.xpi b/browser/components/preferences/tests/addons/pl-dictionary.xpi
new file mode 100644
index 0000000000..cc4da1fa83
--- /dev/null
+++ b/browser/components/preferences/tests/addons/pl-dictionary.xpi
Binary files differ
diff --git a/browser/components/preferences/tests/addons/set_homepage.xpi b/browser/components/preferences/tests/addons/set_homepage.xpi
new file mode 100644
index 0000000000..9aff671021
--- /dev/null
+++ b/browser/components/preferences/tests/addons/set_homepage.xpi
Binary files differ
diff --git a/browser/components/preferences/tests/addons/set_newtab.xpi b/browser/components/preferences/tests/addons/set_newtab.xpi
new file mode 100644
index 0000000000..f11db0b6a8
--- /dev/null
+++ b/browser/components/preferences/tests/addons/set_newtab.xpi
Binary files differ
diff --git a/browser/components/preferences/tests/browser.ini b/browser/components/preferences/tests/browser.ini
new file mode 100644
index 0000000000..8203bceb90
--- /dev/null
+++ b/browser/components/preferences/tests/browser.ini
@@ -0,0 +1,152 @@
+[DEFAULT]
+prefs =
+ extensions.formautofill.addresses.available='on'
+ extensions.formautofill.creditCards.available='on'
+ signon.management.page.os-auth.enabled=true
+support-files =
+ head.js
+ privacypane_tests_perwindow.js
+ addons/pl-dictionary.xpi
+ addons/set_homepage.xpi
+ addons/set_newtab.xpi
+
+[browser_advanced_update.js]
+skip-if = !updater
+[browser_application_xml_handle_internally.js]
+[browser_applications_selection.js]
+[browser_basic_rebuild_fonts_test.js]
+[browser_browser_languages_subdialog.js]
+skip-if =
+ tsan
+ (!debug && os == 'win') # Bug 1518370
+[browser_bug1018066_resetScrollPosition.js]
+[browser_bug1020245_openPreferences_to_paneContent.js]
+[browser_bug1184989_prevent_scrolling_when_preferences_flipped.js]
+skip-if = os == "mac" # 1664576
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+support-files =
+ browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml
+[browser_bug1547020_lockedDownloadDir.js]
+[browser_bug1579418.js]
+[browser_bug410900.js]
+[browser_bug731866.js]
+[browser_bug795764_cachedisabled.js]
+[browser_cert_export.js]
+[browser_change_app_handler.js]
+skip-if = os != "win" # Windows-specific handler application selection dialog
+[browser_checkspelling.js]
+[browser_connection.js]
+[browser_connection_bug1445991.js]
+[browser_connection_bug1505330.js]
+skip-if = (verify && debug && (os == 'linux' || os == 'mac'))
+[browser_connection_bug388287.js]
+[browser_containers_name_input.js]
+[browser_contentblocking.js]
+skip-if = socketprocess_networking
+[browser_contentblocking_categories.js]
+[browser_contentblocking_standard_tcp_section.js]
+[browser_cookie_exceptions_addRemove.js]
+[browser_cookies_exceptions.js]
+[browser_defaultbrowser_alwayscheck.js]
+[browser_engines.js]
+[browser_etp_exceptions_dialog.js]
+[browser_experimental_features.js]
+[browser_experimental_features_filter.js]
+[browser_experimental_features_hidden_when_not_public.js]
+skip-if =
+ os == "mac" && debug # Bug 1723854
+ os == "linux" && os_version == "18.04" && debug # Bug 1723854
+[browser_experimental_features_resetall.js]
+[browser_extension_controlled.js]
+skip-if =
+ tsan
+ ccov && (os == 'linux' || os == 'win') # Linux: bug 1613530, Windows: bug 1437051
+[browser_filetype_dialog.js]
+[browser_fluent.js]
+[browser_homepage_default.js]
+[browser_homepages_filter_aboutpreferences.js]
+[browser_homepages_use_bookmark.js]
+[browser_hometab_restore_defaults.js]
+https_first_disabled = true
+[browser_https_only_exceptions.js]
+[browser_https_only_section.js]
+[browser_ignore_invalid_capability.js]
+[browser_languages_subdialog.js]
+[browser_layersacceleration.js]
+[browser_localSearchShortcuts.js]
+[browser_moreFromMozilla.js]
+[browser_moreFromMozilla_locales.js]
+[browser_newtab_menu.js]
+[browser_notifications_do_not_disturb.js]
+[browser_open_download_preferences.js]
+support-files = empty_pdf_file.pdf
+[browser_open_migration_wizard.js]
+[browser_password_management.js]
+[browser_pdf_disabled.js]
+[browser_performance.js]
+[browser_performance_content_process_limit.js]
+[browser_performance_e10srollout.js]
+[browser_performance_non_e10s.js]
+skip-if = true
+[browser_permissions_checkPermissionsWereAdded.js]
+[browser_permissions_dialog.js]
+[browser_permissions_dialog_default_perm.js]
+[browser_permissions_urlFieldHidden.js]
+[browser_primaryPassword.js]
+[browser_privacy_cookieBannerHandling.js]
+[browser_privacy_dnsoverhttps.js]
+[browser_privacy_firefoxSuggest.js]
+[browser_privacy_passwordGenerationAndAutofill.js]
+[browser_privacy_quickactions.js]
+[browser_privacy_relayIntegration.js]
+[browser_privacy_segmentation_pref.js]
+[browser_privacy_syncDataClearing.js]
+[browser_privacypane_2.js]
+[browser_privacypane_3.js]
+[browser_proxy_backup.js]
+[browser_sanitizeOnShutdown_prefLocked.js]
+[browser_searchChangedEngine.js]
+[browser_searchDefaultEngine.js]
+support-files =
+ engine1/manifest.json
+ engine2/manifest.json
+[browser_searchFindMoreLink.js]
+[browser_searchRestoreDefaults.js]
+[browser_searchScroll.js]
+support-files =
+ !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
+[browser_searchShowSuggestionsFirst.js]
+[browser_search_no_results_change_category.js]
+[browser_search_searchTerms.js]
+[browser_search_subdialog_tooltip_saved_addresses.js]
+[browser_search_subdialogs_within_preferences_1.js]
+skip-if = tsan # Bug 1678829
+[browser_search_subdialogs_within_preferences_2.js]
+[browser_search_subdialogs_within_preferences_3.js]
+[browser_search_subdialogs_within_preferences_4.js]
+[browser_search_subdialogs_within_preferences_5.js]
+[browser_search_subdialogs_within_preferences_6.js]
+[browser_search_subdialogs_within_preferences_7.js]
+[browser_search_subdialogs_within_preferences_8.js]
+[browser_search_subdialogs_within_preferences_site_data.js]
+[browser_search_within_preferences_1.js]
+skip-if = (os == 'win' && processor == "aarch64") # Bug 1536560
+[browser_search_within_preferences_2.js]
+[browser_search_within_preferences_command.js]
+[browser_searchsuggestions.js]
+[browser_security-1.js]
+[browser_security-2.js]
+[browser_security-3.js]
+[browser_site_login_exceptions.js]
+[browser_site_login_exceptions_policy.js]
+[browser_spotlight.js]
+[browser_statePartitioning_PBM_strings.js]
+[browser_statePartitioning_strings.js]
+[browser_subdialogs.js]
+support-files =
+ subdialog.xhtml
+ subdialog2.xhtml
+[browser_sync_chooseWhatToSync.js]
+[browser_sync_disabled.js]
+[browser_sync_pairing.js]
+[browser_warning_permanent_private_browsing.js]
diff --git a/browser/components/preferences/tests/browser_advanced_update.js b/browser/components/preferences/tests/browser_advanced_update.js
new file mode 100644
index 0000000000..95da7a1c7a
--- /dev/null
+++ b/browser/components/preferences/tests/browser_advanced_update.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Cm = Components.manager;
+
+const uuidGenerator = Services.uuid;
+
+const mockUpdateManager = {
+ contractId: "@mozilla.org/updates/update-manager;1",
+
+ _mockClassId: uuidGenerator.generateUUID(),
+
+ _originalClassId: "",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIUpdateManager"]),
+
+ createInstance(iiD) {
+ return this.QueryInterface(iiD);
+ },
+
+ register() {
+ let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ if (!registrar.isCIDRegistered(this._mockClassId)) {
+ this._originalClassId = registrar.contractIDToCID(this.contractId);
+ registrar.registerFactory(
+ this._mockClassId,
+ "Unregister after testing",
+ this.contractId,
+ this
+ );
+ }
+ },
+
+ unregister() {
+ let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(this._mockClassId, this);
+ registrar.registerFactory(this._originalClassId, "", this.contractId, null);
+ },
+
+ getUpdateCount() {
+ return this._updates.length;
+ },
+
+ getUpdateAt(index) {
+ return this._updates[index];
+ },
+
+ _updates: [
+ {
+ name: "Firefox Developer Edition 49.0a2",
+ statusText: "The Update was successfully installed",
+ buildID: "20160728004010",
+ installDate: 1469763105156,
+ detailsURL: "https://www.mozilla.org/firefox/aurora/",
+ },
+ {
+ name: "Firefox Developer Edition 43.0a2",
+ statusText: "The Update was successfully installed",
+ buildID: "20150929004011",
+ installDate: 1443585886224,
+ detailsURL: "https://www.mozilla.org/firefox/aurora/",
+ },
+ {
+ name: "Firefox Developer Edition 42.0a2",
+ statusText: "The Update was successfully installed",
+ buildID: "20150920004018",
+ installDate: 1442818147544,
+ detailsURL: "https://www.mozilla.org/firefox/aurora/",
+ },
+ ],
+};
+
+function formatInstallDate(sec) {
+ var date = new Date(sec);
+ const dtOptions = {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric",
+ };
+ return date.toLocaleString(undefined, dtOptions);
+}
+
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ let showBtn = doc.getElementById("showUpdateHistory");
+ let dialogOverlay = content.gSubDialog._preloadDialog._overlay;
+
+ // XXX: For unknown reasons, this mock cannot be loaded by
+ // XPCOMUtils.defineLazyServiceGetter() called in aboutDialog-appUpdater.js.
+ // It is registered here so that we could assert update history subdialog
+ // without stopping the preferences advanced pane from loading.
+ // See bug 1361929.
+ mockUpdateManager.register();
+
+ // Test the dialog window opens
+ ok(
+ BrowserTestUtils.is_hidden(dialogOverlay),
+ "The dialog should be invisible"
+ );
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://mozapps/content/update/history.xhtml"
+ );
+ showBtn.doCommand();
+ await promiseSubDialogLoaded;
+ ok(
+ !BrowserTestUtils.is_hidden(dialogOverlay),
+ "The dialog should be visible"
+ );
+
+ let dialogFrame = dialogOverlay.querySelector(".dialogFrame");
+ let frameDoc = dialogFrame.contentDocument;
+ let updates = frameDoc.querySelectorAll("richlistitem.update");
+
+ // Test the update history numbers are correct
+ is(
+ updates.length,
+ mockUpdateManager.getUpdateCount(),
+ "The update count is incorrect."
+ );
+
+ // Test the updates are displayed correctly
+ let update = null;
+ let updateData = null;
+ for (let i = 0; i < updates.length; ++i) {
+ update = updates[i];
+ updateData = mockUpdateManager.getUpdateAt(i);
+
+ let testcases = [
+ {
+ selector: ".update-name",
+ id: "update-full-build-name",
+ args: { name: updateData.name, buildID: updateData.buildID },
+ },
+ {
+ selector: ".update-installedOn-label",
+ id: "update-installed-on",
+ args: { date: formatInstallDate(updateData.installDate) },
+ },
+ {
+ selector: ".update-status-label",
+ id: "update-status",
+ args: { status: updateData.statusText },
+ },
+ ];
+
+ for (let { selector, id, args } of testcases) {
+ const element = update.querySelector(selector);
+ const l10nAttrs = frameDoc.l10n.getAttributes(element);
+ Assert.deepEqual(
+ l10nAttrs,
+ {
+ id,
+ args,
+ },
+ "Wrong " + id
+ );
+ }
+
+ if (update.detailsURL) {
+ is(
+ update.detailsURL,
+ update.querySelector(".text-link").href,
+ "Wrong detailsURL"
+ );
+ }
+ }
+
+ // Test the dialog window closes
+ let closeBtn = dialogOverlay.querySelector(".dialogClose");
+ closeBtn.doCommand();
+ ok(
+ BrowserTestUtils.is_hidden(dialogOverlay),
+ "The dialog should be invisible"
+ );
+
+ mockUpdateManager.unregister();
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_application_xml_handle_internally.js b/browser/components/preferences/tests/browser_application_xml_handle_internally.js
new file mode 100644
index 0000000000..edb4a4c0ec
--- /dev/null
+++ b/browser/components/preferences/tests/browser_application_xml_handle_internally.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+
+const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+// This test checks that application/xml has the handle internally option.
+add_task(async function applicationXmlHandleInternally() {
+ const mimeInfo = MIMEService.getFromTypeAndExtension(
+ "application/xml",
+ "xml"
+ );
+ HandlerService.store(mimeInfo);
+ registerCleanupFunction(() => {
+ HandlerService.remove(mimeInfo);
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+
+ let win = gBrowser.selectedBrowser.contentWindow;
+
+ let container = win.document.getElementById("handlersView");
+
+ // First, find the application/xml item.
+ let xmlItem = container.querySelector("richlistitem[type='application/xml']");
+ Assert.ok(xmlItem, "application/xml is present in handlersView");
+ if (xmlItem) {
+ xmlItem.scrollIntoView({ block: "center" });
+ xmlItem.closest("richlistbox").selectItem(xmlItem);
+
+ // Open its menu
+ let list = xmlItem.querySelector(".actionsMenu");
+ let popup = list.menupopup;
+ let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(list, {}, win);
+ await popupShown;
+
+ let handleInternallyItem = list.querySelector(
+ `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']`
+ );
+
+ ok(!!handleInternallyItem, "handle internally is present");
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_applications_selection.js b/browser/components/preferences/tests/browser_applications_selection.js
new file mode 100644
index 0000000000..683ce76a89
--- /dev/null
+++ b/browser/components/preferences/tests/browser_applications_selection.js
@@ -0,0 +1,403 @@
+SimpleTest.requestCompleteLog();
+const { HandlerServiceTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/HandlerServiceTestUtils.sys.mjs"
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+let gOldMailHandlers = [];
+let gDummyHandlers = [];
+let gOriginalPreferredMailHandler;
+let gOriginalPreferredPDFHandler;
+
+registerCleanupFunction(function () {
+ function removeDummyHandlers(handlers) {
+ // Remove any of the dummy handlers we created.
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ if (
+ gDummyHandlers.some(
+ h =>
+ h.uriTemplate ==
+ handlers.queryElementAt(i, Ci.nsIWebHandlerApp).uriTemplate
+ )
+ ) {
+ handlers.removeElementAt(i);
+ }
+ } catch (ex) {
+ /* ignore non-web-app handlers */
+ }
+ }
+ }
+ // Re-add the original protocol handlers:
+ let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
+ let mailHandlers = mailHandlerInfo.possibleApplicationHandlers;
+ for (let h of gOldMailHandlers) {
+ mailHandlers.appendElement(h);
+ }
+ removeDummyHandlers(mailHandlers);
+ mailHandlerInfo.preferredApplicationHandler = gOriginalPreferredMailHandler;
+ gHandlerService.store(mailHandlerInfo);
+
+ let pdfHandlerInfo =
+ HandlerServiceTestUtils.getHandlerInfo("application/pdf");
+ removeDummyHandlers(pdfHandlerInfo.possibleApplicationHandlers);
+ pdfHandlerInfo.preferredApplicationHandler = gOriginalPreferredPDFHandler;
+ gHandlerService.store(pdfHandlerInfo);
+
+ gBrowser.removeCurrentTab();
+});
+
+function scrubMailtoHandlers(handlerInfo) {
+ // Remove extant web handlers because they have icons that
+ // we fetch from the web, which isn't allowed in tests.
+ let handlers = handlerInfo.possibleApplicationHandlers;
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ gOldMailHandlers.push(handler);
+ // If we get here, this is a web handler app. Remove it:
+ handlers.removeElementAt(i);
+ } catch (ex) {}
+ }
+}
+
+add_setup(async function () {
+ // Create our dummy handlers
+ let handler1 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ handler1.name = "Handler 1";
+ handler1.uriTemplate = "https://example.com/first/%s";
+
+ let handler2 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ handler2.name = "Handler 2";
+ handler2.uriTemplate = "http://example.org/second/%s";
+ gDummyHandlers.push(handler1, handler2);
+
+ function substituteWebHandlers(handlerInfo) {
+ // Append the dummy handlers to replace them:
+ let handlers = handlerInfo.possibleApplicationHandlers;
+ handlers.appendElement(handler1);
+ handlers.appendElement(handler2);
+ gHandlerService.store(handlerInfo);
+ }
+ // Set up our mailto handler test infrastructure.
+ let mailtoHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
+ scrubMailtoHandlers(mailtoHandlerInfo);
+ gOriginalPreferredMailHandler = mailtoHandlerInfo.preferredApplicationHandler;
+ substituteWebHandlers(mailtoHandlerInfo);
+
+ // Now do the same for pdf handler:
+ let pdfHandlerInfo =
+ HandlerServiceTestUtils.getHandlerInfo("application/pdf");
+ // PDF doesn't have built-in web handlers, so no need to scrub.
+ gOriginalPreferredPDFHandler = pdfHandlerInfo.preferredApplicationHandler;
+ substituteWebHandlers(pdfHandlerInfo);
+
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ info("Preferences page opened on the general pane.");
+
+ await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList;
+ info("Apps list loaded.");
+});
+
+async function selectStandardOptions(itemToUse) {
+ async function selectItemInPopup(item) {
+ let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ // Synthesizing the mouse on the .actionsMenu menulist somehow just selects
+ // the top row. Probably something to do with the multiple layers of anon
+ // content - workaround by using the `.open` setter instead.
+ list.open = true;
+ await popupShown;
+ let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ if (typeof item == "function") {
+ item = item();
+ }
+ popup.activateItem(item);
+ await popupHidden;
+ return item;
+ }
+
+ let itemType = itemToUse.getAttribute("type");
+ // Center the item. Center rather than top so it doesn't get blocked by
+ // the search header.
+ itemToUse.scrollIntoView({ block: "center" });
+ itemToUse.closest("richlistbox").selectItem(itemToUse);
+ Assert.ok(itemToUse.selected, "Should be able to select our item.");
+ // Force reflow to make sure it's visible and the container dropdown isn't
+ // hidden.
+ itemToUse.getBoundingClientRect().top;
+ let list = itemToUse.querySelector(".actionsMenu");
+ let popup = list.menupopup;
+
+ // select one of our test cases:
+ let handlerItem = list.querySelector("menuitem[data-l10n-args*='Handler 1']");
+ await selectItemInPopup(handlerItem);
+ let { preferredAction, alwaysAskBeforeHandling } =
+ HandlerServiceTestUtils.getHandlerInfo(itemType);
+ Assert.notEqual(
+ preferredAction,
+ Ci.nsIHandlerInfo.alwaysAsk,
+ "Should have selected something other than 'always ask' (" + itemType + ")"
+ );
+ Assert.ok(
+ !alwaysAskBeforeHandling,
+ "Should have turned off asking before handling (" + itemType + ")"
+ );
+
+ // Test the alwaysAsk option
+ let alwaysAskItem = list.getElementsByAttribute(
+ "action",
+ Ci.nsIHandlerInfo.alwaysAsk
+ )[0];
+ await selectItemInPopup(alwaysAskItem);
+ Assert.equal(
+ list.selectedItem,
+ alwaysAskItem,
+ "Should have selected always ask item (" + itemType + ")"
+ );
+ alwaysAskBeforeHandling =
+ HandlerServiceTestUtils.getHandlerInfo(itemType).alwaysAskBeforeHandling;
+ Assert.ok(
+ alwaysAskBeforeHandling,
+ "Should have turned on asking before handling (" + itemType + ")"
+ );
+
+ let useDefaultItem = list.getElementsByAttribute(
+ "action",
+ Ci.nsIHandlerInfo.useSystemDefault
+ );
+ useDefaultItem = useDefaultItem && useDefaultItem[0];
+ if (useDefaultItem) {
+ await selectItemInPopup(useDefaultItem);
+ Assert.equal(
+ list.selectedItem,
+ useDefaultItem,
+ "Should have selected 'use default' item (" + itemType + ")"
+ );
+ preferredAction =
+ HandlerServiceTestUtils.getHandlerInfo(itemType).preferredAction;
+ Assert.equal(
+ preferredAction,
+ Ci.nsIHandlerInfo.useSystemDefault,
+ "Should have selected 'use default' (" + itemType + ")"
+ );
+ } else {
+ // Whether there's a "use default" item depends on the OS, so it's not
+ // possible to rely on it being the case or not.
+ info("No 'Use default' item, so not testing (" + itemType + ")");
+ }
+
+ // Select a web app item.
+ let webAppItems = Array.from(
+ popup.getElementsByAttribute("action", Ci.nsIHandlerInfo.useHelperApp)
+ );
+ webAppItems = webAppItems.filter(
+ item => item.handlerApp instanceof Ci.nsIWebHandlerApp
+ );
+ Assert.equal(
+ webAppItems.length,
+ 2,
+ "Should have 2 web application handler. (" + itemType + ")"
+ );
+ Assert.notEqual(
+ webAppItems[0].label,
+ webAppItems[1].label,
+ "Should have 2 different web app handlers"
+ );
+ let selectedItem = await selectItemInPopup(webAppItems[0]);
+
+ // Test that the selected item label is the same as the label
+ // of the menu item.
+ let win = gBrowser.selectedBrowser.contentWindow;
+ await win.document.l10n.translateFragment(selectedItem);
+ await win.document.l10n.translateFragment(itemToUse);
+ Assert.equal(
+ selectedItem.label,
+ itemToUse.querySelector(".actionContainer label").value,
+ "Should have selected correct item (" + itemType + ")"
+ );
+ let { preferredApplicationHandler } =
+ HandlerServiceTestUtils.getHandlerInfo(itemType);
+ preferredApplicationHandler.QueryInterface(Ci.nsIWebHandlerApp);
+ Assert.equal(
+ selectedItem.handlerApp.uriTemplate,
+ preferredApplicationHandler.uriTemplate,
+ "App should actually be selected in the backend. (" + itemType + ")"
+ );
+
+ // select the other web app item
+ selectedItem = await selectItemInPopup(webAppItems[1]);
+
+ // Test that the selected item label is the same as the label
+ // of the menu item
+ await win.document.l10n.translateFragment(selectedItem);
+ await win.document.l10n.translateFragment(itemToUse);
+ Assert.equal(
+ selectedItem.label,
+ itemToUse.querySelector(".actionContainer label").value,
+ "Should have selected correct item (" + itemType + ")"
+ );
+ preferredApplicationHandler =
+ HandlerServiceTestUtils.getHandlerInfo(
+ itemType
+ ).preferredApplicationHandler;
+ preferredApplicationHandler.QueryInterface(Ci.nsIWebHandlerApp);
+ Assert.equal(
+ selectedItem.handlerApp.uriTemplate,
+ preferredApplicationHandler.uriTemplate,
+ "App should actually be selected in the backend. (" + itemType + ")"
+ );
+}
+
+add_task(async function checkDropdownBehavior() {
+ let win = gBrowser.selectedBrowser.contentWindow;
+
+ let container = win.document.getElementById("handlersView");
+
+ // First check a protocol handler item.
+ let mailItem = container.querySelector("richlistitem[type='mailto']");
+ Assert.ok(mailItem, "mailItem is present in handlersView.");
+ await selectStandardOptions(mailItem);
+
+ // Then check a content menu item.
+ let pdfItem = container.querySelector("richlistitem[type='application/pdf']");
+ Assert.ok(pdfItem, "pdfItem is present in handlersView.");
+ await selectStandardOptions(pdfItem);
+});
+
+add_task(async function sortingCheck() {
+ let win = gBrowser.selectedBrowser.contentWindow;
+ const handlerView = win.document.getElementById("handlersView");
+ const typeColumn = win.document.getElementById("typeColumn");
+ Assert.ok(typeColumn, "typeColumn is present in handlersView.");
+
+ let expectedNumberOfItems =
+ handlerView.querySelectorAll("richlistitem").length;
+
+ // Test default sorting
+ assertSortByType("ascending");
+
+ const oldDir = typeColumn.getAttribute("sortDirection");
+
+ // click on an item and sort again:
+ let itemToUse = handlerView.querySelector("richlistitem[type=mailto]");
+ itemToUse.scrollIntoView({ block: "center" });
+ itemToUse.closest("richlistbox").selectItem(itemToUse);
+
+ // Test sorting on the type column
+ typeColumn.click();
+ assertSortByType("descending");
+ Assert.notEqual(
+ oldDir,
+ typeColumn.getAttribute("sortDirection"),
+ "Sort direction should change"
+ );
+
+ typeColumn.click();
+ assertSortByType("ascending");
+
+ const actionColumn = win.document.getElementById("actionColumn");
+ Assert.ok(actionColumn, "actionColumn is present in handlersView.");
+
+ // Test sorting on the action column
+ const oldActionDir = actionColumn.getAttribute("sortDirection");
+ actionColumn.click();
+ assertSortByAction("ascending");
+ Assert.notEqual(
+ oldActionDir,
+ actionColumn.getAttribute("sortDirection"),
+ "Sort direction should change"
+ );
+
+ actionColumn.click();
+ assertSortByAction("descending");
+
+ // Restore the default sort order
+ typeColumn.click();
+ assertSortByType("ascending");
+
+ function assertSortByAction(order) {
+ Assert.equal(
+ actionColumn.getAttribute("sortDirection"),
+ order,
+ `Sort direction should be ${order}`
+ );
+ let siteItems = handlerView.getElementsByTagName("richlistitem");
+ Assert.equal(
+ siteItems.length,
+ expectedNumberOfItems,
+ "Number of items should not change."
+ );
+ for (let i = 0; i < siteItems.length - 1; ++i) {
+ let aType = siteItems[i].getAttribute("actionDescription").toLowerCase();
+ let bType = siteItems[i + 1]
+ .getAttribute("actionDescription")
+ .toLowerCase();
+ let result = 0;
+ if (aType > bType) {
+ result = 1;
+ } else if (bType > aType) {
+ result = -1;
+ }
+ if (order == "ascending") {
+ Assert.lessOrEqual(
+ result,
+ 0,
+ "Should sort applications in the ascending order by action"
+ );
+ } else {
+ Assert.greaterOrEqual(
+ result,
+ 0,
+ "Should sort applications in the descending order by action"
+ );
+ }
+ }
+ }
+
+ function assertSortByType(order) {
+ Assert.equal(
+ typeColumn.getAttribute("sortDirection"),
+ order,
+ `Sort direction should be ${order}`
+ );
+
+ let siteItems = handlerView.getElementsByTagName("richlistitem");
+ Assert.equal(
+ siteItems.length,
+ expectedNumberOfItems,
+ "Number of items should not change."
+ );
+ for (let i = 0; i < siteItems.length - 1; ++i) {
+ let aType = siteItems[i].getAttribute("typeDescription").toLowerCase();
+ let bType = siteItems[i + 1]
+ .getAttribute("typeDescription")
+ .toLowerCase();
+ let result = 0;
+ if (aType > bType) {
+ result = 1;
+ } else if (bType > aType) {
+ result = -1;
+ }
+ if (order == "ascending") {
+ Assert.lessOrEqual(
+ result,
+ 0,
+ "Should sort applications in the ascending order by type"
+ );
+ } else {
+ Assert.greaterOrEqual(
+ result,
+ 0,
+ "Should sort applications in the descending order by type"
+ );
+ }
+ }
+ }
+});
diff --git a/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js b/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js
new file mode 100644
index 0000000000..bacda8a6b4
--- /dev/null
+++ b/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js
@@ -0,0 +1,235 @@
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ await gBrowser.contentWindow.gMainPane._selectDefaultLanguageGroupPromise;
+ await TestUtils.waitForCondition(
+ () => !gBrowser.contentWindow.Preferences.updateQueued
+ );
+
+ let doc = gBrowser.contentDocument;
+ let contentWindow = gBrowser.contentWindow;
+ var langGroup = Services.prefs.getComplexValue(
+ "font.language.group",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ is(
+ contentWindow.Preferences.get("font.language.group").value,
+ langGroup,
+ "Language group should be set correctly."
+ );
+
+ let defaultFontType = Services.prefs.getCharPref("font.default." + langGroup);
+ let fontFamilyPref = "font.name." + defaultFontType + "." + langGroup;
+ let fontFamily = Services.prefs.getCharPref(fontFamilyPref);
+ let fontFamilyField = doc.getElementById("defaultFont");
+ is(fontFamilyField.value, fontFamily, "Font family should be set correctly.");
+
+ function dispatchMenuItemCommand(menuItem) {
+ const cmdEvent = doc.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ contentWindow,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ 0
+ );
+ menuItem.dispatchEvent(cmdEvent);
+ }
+
+ /**
+ * Return a promise that resolves when the fontFamilyPref changes.
+ *
+ * Font prefs are the only ones whose form controls set "delayprefsave",
+ * which delays the pref change when a user specifies a new value
+ * for the pref. Thus, in order to confirm that the pref gets changed
+ * when the test selects a new value in a font field, we need to await
+ * the change. Awaiting this function does so for fontFamilyPref.
+ */
+ function fontFamilyPrefChanged() {
+ return new Promise(resolve => {
+ const observer = {
+ observe(aSubject, aTopic, aData) {
+ // Check for an exact match to avoid the ambiguity of nsIPrefBranch's
+ // prefix-matching algorithm for notifying pref observers.
+ if (aData == fontFamilyPref) {
+ Services.prefs.removeObserver(fontFamilyPref, observer);
+ resolve();
+ }
+ },
+ };
+ Services.prefs.addObserver(fontFamilyPref, observer);
+ });
+ }
+
+ const menuItems = fontFamilyField.querySelectorAll("menuitem");
+ ok(menuItems.length > 1, "There are multiple font menuitems.");
+ ok(menuItems[0].selected, "The first (default) font menuitem is selected.");
+
+ dispatchMenuItemCommand(menuItems[1]);
+ ok(menuItems[1].selected, "The second font menuitem is selected.");
+
+ await fontFamilyPrefChanged();
+ fontFamily = Services.prefs.getCharPref(fontFamilyPref);
+ is(fontFamilyField.value, fontFamily, "The font family has been updated.");
+
+ dispatchMenuItemCommand(menuItems[0]);
+ ok(
+ menuItems[0].selected,
+ "The first (default) font menuitem is selected again."
+ );
+
+ await fontFamilyPrefChanged();
+ fontFamily = Services.prefs.getCharPref(fontFamilyPref);
+ is(fontFamilyField.value, fontFamily, "The font family has been updated.");
+
+ let defaultFontSize = Services.prefs.getIntPref(
+ "font.size.variable." + langGroup
+ );
+ let fontSizeField = doc.getElementById("defaultFontSize");
+ is(
+ fontSizeField.value,
+ "" + defaultFontSize,
+ "Font size should be set correctly."
+ );
+
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/fonts.xhtml"
+ );
+ doc.getElementById("advancedFonts").click();
+ let win = await promiseSubDialogLoaded;
+ doc = win.document;
+
+ // Simulate a dumb font backend.
+ win.FontBuilder._enumerator = {
+ _list: ["MockedFont1", "MockedFont2", "MockedFont3"],
+ _defaultFont: null,
+ EnumerateFontsAsync(lang, type) {
+ return Promise.resolve(this._list);
+ },
+ EnumerateAllFontsAsync() {
+ return Promise.resolve(this._list);
+ },
+ getDefaultFont() {
+ return this._defaultFont;
+ },
+ getStandardFamilyName(name) {
+ return name;
+ },
+ };
+ win.FontBuilder._allFonts = null;
+ win.FontBuilder._langGroupSupported = false;
+
+ let langGroupElement = win.Preferences.get("font.language.group");
+ let selectLangsField = doc.getElementById("selectLangs");
+ let serifField = doc.getElementById("serif");
+ let armenian = "x-armn";
+ let western = "x-western";
+
+ // Await rebuilding of the font lists, which happens asynchronously in
+ // gFontsDialog._selectLanguageGroup. Testing code needs to call this
+ // function and await its resolution after changing langGroupElement's value
+ // (or doing anything else that triggers a call to _selectLanguageGroup).
+ function fontListsRebuilt() {
+ return win.gFontsDialog._selectLanguageGroupPromise;
+ }
+
+ langGroupElement.value = armenian;
+ await fontListsRebuilt();
+ selectLangsField.value = armenian;
+ is(serifField.value, "", "Font family should not be set.");
+
+ let armenianSerifElement = win.Preferences.get("font.name.serif.x-armn");
+
+ langGroupElement.value = western;
+ await fontListsRebuilt();
+ selectLangsField.value = western;
+
+ // Simulate a font backend supporting language-specific enumeration.
+ // NB: FontBuilder has cached the return value from EnumerateAllFonts(),
+ // so _allFonts will always have 3 elements regardless of subsequent
+ // _list changes.
+ win.FontBuilder._enumerator._list = ["MockedFont2"];
+
+ langGroupElement.value = armenian;
+ await fontListsRebuilt();
+ selectLangsField.value = armenian;
+ is(
+ serifField.value,
+ "",
+ "Font family should still be empty for indicating using 'default' font."
+ );
+
+ langGroupElement.value = western;
+ await fontListsRebuilt();
+ selectLangsField.value = western;
+
+ // Simulate a system that has no fonts for the specified language.
+ win.FontBuilder._enumerator._list = [];
+
+ langGroupElement.value = armenian;
+ await fontListsRebuilt();
+ selectLangsField.value = armenian;
+ is(serifField.value, "", "Font family should not be set.");
+
+ // Setting default font to "MockedFont3". Then, when serifField.value is
+ // empty, it should indicate using "MockedFont3" but it shouldn't be saved
+ // to "MockedFont3" in the pref. It should be resolved at runtime.
+ win.FontBuilder._enumerator._list = [
+ "MockedFont1",
+ "MockedFont2",
+ "MockedFont3",
+ ];
+ win.FontBuilder._enumerator._defaultFont = "MockedFont3";
+ langGroupElement.value = armenian;
+ await fontListsRebuilt();
+ selectLangsField.value = armenian;
+ is(
+ serifField.value,
+ "",
+ "Font family should be empty even if there is a default font."
+ );
+
+ armenianSerifElement.value = "MockedFont2";
+ serifField.value = "MockedFont2";
+ is(
+ serifField.value,
+ "MockedFont2",
+ 'Font family should be "MockedFont2" for now.'
+ );
+
+ langGroupElement.value = western;
+ await fontListsRebuilt();
+ selectLangsField.value = western;
+ is(serifField.value, "", "Font family of other language should not be set.");
+
+ langGroupElement.value = armenian;
+ await fontListsRebuilt();
+ selectLangsField.value = armenian;
+ is(
+ serifField.value,
+ "MockedFont2",
+ "Font family should not be changed even after switching the language."
+ );
+
+ // If MochedFont2 is removed from the system, the value should be treated
+ // as empty (i.e., 'default' font) after rebuilding the font list.
+ win.FontBuilder._enumerator._list = ["MockedFont1", "MockedFont3"];
+ win.FontBuilder._enumerator._allFonts = ["MockedFont1", "MockedFont3"];
+ serifField.removeAllItems(); // This will cause rebuilding the font list from available fonts.
+ langGroupElement.value = armenian;
+ await fontListsRebuilt();
+ selectLangsField.value = armenian;
+ is(
+ serifField.value,
+ "",
+ "Font family should become empty due to the font uninstalled."
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_browser_languages_subdialog.js b/browser/components/preferences/tests/browser_browser_languages_subdialog.js
new file mode 100644
index 0000000000..8b57bf08a8
--- /dev/null
+++ b/browser/components/preferences/tests/browser_browser_languages_subdialog.js
@@ -0,0 +1,1058 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const BROWSER_LANGUAGES_URL =
+ "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml";
+const DICTIONARY_ID_PL = "pl@dictionaries.addons.mozilla.org";
+const TELEMETRY_CATEGORY = "intl.ui.browserLanguage";
+
+function langpackId(locale) {
+ return `langpack-${locale}@firefox.mozilla.org`;
+}
+
+function getManifestData(locale, version = "2.0") {
+ return {
+ langpack_id: locale,
+ name: `${locale} Language Pack`,
+ description: `${locale} Language pack`,
+ languages: {
+ [locale]: {
+ chrome_resources: {
+ branding: `browser/chrome/${locale}/locale/branding/`,
+ },
+ version: "1",
+ },
+ },
+ browser_specific_settings: {
+ gecko: {
+ id: langpackId(locale),
+ strict_min_version: AppConstants.MOZ_APP_VERSION,
+ strict_max_version: AppConstants.MOZ_APP_VERSION,
+ },
+ },
+ version,
+ manifest_version: 2,
+ sources: {
+ browser: {
+ base_path: "browser/",
+ },
+ },
+ author: "Mozilla",
+ };
+}
+
+let testLocales = ["fr", "pl", "he"];
+let testLangpacks;
+
+function createLangpack(locale, version) {
+ return AddonTestUtils.createTempXPIFile({
+ "manifest.json": getManifestData(locale, version),
+ [`browser/${locale}/branding/brand.ftl`]: "-brand-short-name = Firefox",
+ });
+}
+
+function createTestLangpacks() {
+ if (!testLangpacks) {
+ testLangpacks = Promise.all(
+ testLocales.map(async locale => [locale, await createLangpack(locale)])
+ );
+ }
+ return testLangpacks;
+}
+
+function createLocaleResult(target_locale, url) {
+ return {
+ guid: langpackId(target_locale),
+ type: "language",
+ target_locale,
+ current_compatible_version: {
+ files: [
+ {
+ platform: "all",
+ url,
+ },
+ ],
+ },
+ };
+}
+
+async function createLanguageToolsFile() {
+ let langpacks = await createTestLangpacks();
+ let results = langpacks.map(([locale, file]) =>
+ createLocaleResult(locale, Services.io.newFileURI(file).spec)
+ );
+
+ let filename = "language-tools.json";
+ let files = { [filename]: { results } };
+ let tempdir = AddonTestUtils.tempDir.clone();
+ let dir = await AddonTestUtils.promiseWriteFilesToDir(tempdir.path, files);
+ dir.append(filename);
+
+ return dir;
+}
+
+async function createDictionaryBrowseResults() {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ let dictionaryPath = testDir + "/addons/pl-dictionary.xpi";
+ let filename = "dictionaries.json";
+ let response = {
+ page_size: 25,
+ page_count: 1,
+ count: 1,
+ results: [
+ {
+ current_version: {
+ id: 1823648,
+ compatibility: {
+ firefox: { max: "9999", min: "4.0" },
+ },
+ files: [
+ {
+ platform: "all",
+ url: dictionaryPath,
+ },
+ ],
+ version: "1.0.20160228",
+ },
+ default_locale: "pl",
+ description: "Polish spell-check",
+ guid: DICTIONARY_ID_PL,
+ name: "Polish Dictionary",
+ slug: "polish-spellchecker-dictionary",
+ status: "public",
+ summary: "Polish dictionary",
+ type: "dictionary",
+ },
+ ],
+ };
+
+ let files = { [filename]: response };
+ let dir = await AddonTestUtils.promiseWriteFilesToDir(
+ AddonTestUtils.tempDir.path,
+ files
+ );
+ dir.append(filename);
+
+ return dir;
+}
+
+function assertLocaleOrder(list, locales) {
+ is(
+ list.itemCount,
+ locales.split(",").length,
+ "The right number of locales are selected"
+ );
+ is(
+ Array.from(list.children)
+ .map(child => child.value)
+ .join(","),
+ locales,
+ "The selected locales are in order"
+ );
+}
+
+function assertAvailableLocales(list, locales) {
+ let items = Array.from(list.menupopup.children);
+ let listLocales = items.filter(item => item.value && item.value != "search");
+ is(
+ listLocales.length,
+ locales.length,
+ "The right number of locales are available"
+ );
+ is(
+ listLocales
+ .map(item => item.value)
+ .sort()
+ .join(","),
+ locales.sort().join(","),
+ "The available locales match"
+ );
+ is(items[0].getAttribute("class"), "label-item", "The first row is a label");
+}
+
+function getDialogId(dialogDoc) {
+ return dialogDoc.ownerGlobal.arguments[0].telemetryId;
+}
+
+function assertTelemetryRecorded(events) {
+ let snapshot = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ );
+
+ // Make sure we got some data.
+ ok(
+ snapshot.parent && !!snapshot.parent.length,
+ "Got parent telemetry events in the snapshot"
+ );
+
+ // Only look at the related events after stripping the timestamp and category.
+ let relatedEvents = snapshot.parent
+ .filter(([timestamp, category]) => category == TELEMETRY_CATEGORY)
+ .map(relatedEvent => relatedEvent.slice(2, 6));
+
+ // Events are now an array of: method, object[, value[, extra]] as expected.
+ Assert.deepEqual(relatedEvents, events, "The events are recorded correctly");
+}
+
+async function selectLocale(localeCode, available, selected, dialogDoc) {
+ let [locale] = Array.from(available.menupopup.children).filter(
+ item => item.value == localeCode
+ );
+ available.selectedItem = locale;
+
+ // Get ready for the selected list to change.
+ let added = waitForMutation(selected, { childList: true }, target =>
+ Array.from(target.children).some(el => el.value == localeCode)
+ );
+
+ // Add the locale.
+ dialogDoc.getElementById("add").doCommand();
+
+ // Wait for the list to update.
+ await added;
+}
+
+async function openDialog(doc, search = false) {
+ let dialogLoaded = promiseLoadSubDialog(BROWSER_LANGUAGES_URL);
+ if (search) {
+ doc.getElementById("primaryBrowserLocaleSearch").doCommand();
+ doc.getElementById("primaryBrowserLocale").menupopup.hidePopup();
+ } else {
+ doc.getElementById("manageBrowserLanguagesButton").doCommand();
+ }
+ let dialogWin = await dialogLoaded;
+ let dialogDoc = dialogWin.document;
+ return {
+ dialog: dialogDoc.getElementById("BrowserLanguagesDialog"),
+ dialogDoc,
+ available: dialogDoc.getElementById("availableLocales"),
+ selected: dialogDoc.getElementById("selectedLocales"),
+ };
+}
+
+add_task(async function testDisabledBrowserLanguages() {
+ let langpacksFile = await createLanguageToolsFile();
+ let langpacksUrl = Services.io.newFileURI(langpacksFile).spec;
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", false],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ["intl.locale.requested", "en-US,pl,he,de"],
+ ["extensions.langpacks.signatures.required", false],
+ ["extensions.getAddons.langpacks.url", langpacksUrl],
+ ],
+ });
+
+ // Install an old pl langpack.
+ let oldLangpack = await createLangpack("pl", "1.0");
+ await AddonTestUtils.promiseInstallFile(oldLangpack);
+
+ // Install all the other available langpacks.
+ let pl;
+ let langpacks = await createTestLangpacks();
+ let addons = await Promise.all(
+ langpacks.map(async ([locale, file]) => {
+ if (locale == "pl") {
+ pl = await AddonManager.getAddonByID(langpackId("pl"));
+ // Disable pl so it's removed from selected.
+ await pl.disable();
+ return pl;
+ }
+ let install = await AddonTestUtils.promiseInstallFile(file);
+ return install.addon;
+ })
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let { dialogDoc, available, selected } = await openDialog(doc);
+
+ // pl is not selected since it's disabled.
+ is(pl.userDisabled, true, "pl is disabled");
+ is(pl.version, "1.0", "pl is the old 1.0 version");
+ assertLocaleOrder(selected, "en-US,he");
+
+ // Wait for the children menu to be populated.
+ await BrowserTestUtils.waitForCondition(
+ () => !!available.children.length,
+ "Children list populated"
+ );
+
+ // Only fr is enabled and not selected, so it's the only locale available.
+ assertAvailableLocales(available, ["fr"]);
+
+ // Search for more languages.
+ available.menupopup.lastElementChild.doCommand();
+ available.menupopup.hidePopup();
+ await waitForMutation(available.menupopup, { childList: true }, target =>
+ Array.from(available.menupopup.children).some(
+ locale => locale.value == "pl"
+ )
+ );
+
+ // pl is now available since it is available remotely.
+ assertAvailableLocales(available, ["fr", "pl"]);
+
+ let installId = null;
+ AddonTestUtils.promiseInstallEvent("onInstallEnded").then(([install]) => {
+ installId = install.installId;
+ });
+
+ // Add pl.
+ await selectLocale("pl", available, selected, dialogDoc);
+ assertLocaleOrder(selected, "pl,en-US,he");
+
+ // Find pl again since it's been upgraded.
+ pl = await AddonManager.getAddonByID(langpackId("pl"));
+ is(pl.userDisabled, false, "pl is now enabled");
+ is(pl.version, "2.0", "pl is upgraded to version 2.0");
+
+ let dialogId = getDialogId(dialogDoc);
+ ok(dialogId, "There's a dialogId");
+ ok(installId, "There's an installId");
+
+ await Promise.all(addons.map(addon => addon.uninstall()));
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([
+ ["manage", "main", dialogId],
+ ["search", "dialog", dialogId],
+ ["add", "dialog", dialogId, { installId }],
+
+ // Cancel is recorded when the tab is closed.
+ ["cancel", "dialog", dialogId],
+ ]);
+});
+
+add_task(async function testReorderingBrowserLanguages() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", false],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ["intl.locale.requested", "en-US,pl,he,de"],
+ ["extensions.langpacks.signatures.required", false],
+ ],
+ });
+
+ // Install all the available langpacks.
+ let langpacks = await createTestLangpacks();
+ let addons = await Promise.all(
+ langpacks.map(async ([locale, file]) => {
+ let install = await AddonTestUtils.promiseInstallFile(file);
+ return install.addon;
+ })
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let messageBar = doc.getElementById("confirmBrowserLanguage");
+ is(messageBar.hidden, true, "The message bar is hidden at first");
+
+ // Open the dialog.
+ let { dialog, dialogDoc, selected } = await openDialog(doc);
+ let firstDialogId = getDialogId(dialogDoc);
+
+ // The initial order is set by the pref, filtered by available.
+ assertLocaleOrder(selected, "en-US,pl,he");
+
+ // Moving pl down changes the order.
+ selected.selectedItem = selected.querySelector("[value='pl']");
+ dialogDoc.getElementById("down").doCommand();
+ assertLocaleOrder(selected, "en-US,he,pl");
+
+ // Accepting the change shows the confirm message bar.
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing");
+ dialog.acceptDialog();
+ await dialogClosed;
+
+ // The message bar uses async `formatValues` and that may resolve
+ // after the dialog is closed.
+ await BrowserTestUtils.waitForMutationCondition(
+ messageBar,
+ { attributes: true },
+ () => !messageBar.hidden
+ );
+ is(
+ messageBar.querySelector("button").getAttribute("locales"),
+ "en-US,he,pl",
+ "The locales are set on the message bar button"
+ );
+
+ // Open the dialog again.
+ let newDialog = await openDialog(doc);
+ dialog = newDialog.dialog;
+ dialogDoc = newDialog.dialogDoc;
+ let secondDialogId = getDialogId(dialogDoc);
+ selected = newDialog.selected;
+
+ // The initial order comes from the previous settings.
+ assertLocaleOrder(selected, "en-US,he,pl");
+
+ // Select pl in the list.
+ selected.selectedItem = selected.querySelector("[value='pl']");
+ // Move pl back up.
+ dialogDoc.getElementById("up").doCommand();
+ assertLocaleOrder(selected, "en-US,pl,he");
+
+ // Accepting the change hides the confirm message bar.
+ dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing");
+ dialog.acceptDialog();
+ await dialogClosed;
+ is(messageBar.hidden, true, "The message bar is hidden again");
+
+ ok(firstDialogId, "There was an id on the first dialog");
+ ok(secondDialogId, "There was an id on the second dialog");
+ ok(firstDialogId != secondDialogId, "The dialog ids are different");
+ ok(
+ parseInt(firstDialogId) < parseInt(secondDialogId),
+ "The second dialog id is larger than the first"
+ );
+
+ await Promise.all(addons.map(addon => addon.uninstall()));
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([
+ ["manage", "main", firstDialogId],
+ ["reorder", "dialog", firstDialogId],
+ ["accept", "dialog", firstDialogId],
+ ["manage", "main", secondDialogId],
+ ["reorder", "dialog", secondDialogId],
+ ["accept", "dialog", secondDialogId],
+ ]);
+});
+
+add_task(async function testAddAndRemoveSelectedLanguages() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", false],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ["intl.locale.requested", "en-US"],
+ ["extensions.langpacks.signatures.required", false],
+ ],
+ });
+
+ let langpacks = await createTestLangpacks();
+ let addons = await Promise.all(
+ langpacks.map(async ([locale, file]) => {
+ let install = await AddonTestUtils.promiseInstallFile(file);
+ return install.addon;
+ })
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let messageBar = doc.getElementById("confirmBrowserLanguage");
+ is(messageBar.hidden, true, "The message bar is hidden at first");
+
+ // Open the dialog.
+ let { dialog, dialogDoc, available, selected } = await openDialog(doc);
+ let dialogId = getDialogId(dialogDoc);
+
+ // loadLocalesFromAMO is async but `initAvailableLocales` doesn't wait
+ // for it to be resolved, so we have to wait for the list to be populated
+ // before we test for its values.
+ await BrowserTestUtils.waitForMutationCondition(
+ available.menupopup,
+ { attributes: true, childList: true },
+ () => {
+ let listLocales = Array.from(available.menupopup.children).filter(
+ item => item.value && item.value != "search"
+ );
+ return listLocales.length == 3;
+ }
+ );
+ // The initial order is set by the pref.
+ assertLocaleOrder(selected, "en-US");
+ assertAvailableLocales(available, ["fr", "pl", "he"]);
+
+ // Add pl and fr to selected.
+ await selectLocale("pl", available, selected, dialogDoc);
+ await selectLocale("fr", available, selected, dialogDoc);
+
+ assertLocaleOrder(selected, "fr,pl,en-US");
+ assertAvailableLocales(available, ["he"]);
+
+ // Remove pl and fr from selected.
+ dialogDoc.getElementById("remove").doCommand();
+ dialogDoc.getElementById("remove").doCommand();
+ assertLocaleOrder(selected, "en-US");
+ assertAvailableLocales(available, ["fr", "pl", "he"]);
+
+ // Add he to selected.
+ await selectLocale("he", available, selected, dialogDoc);
+ assertLocaleOrder(selected, "he,en-US");
+ assertAvailableLocales(available, ["pl", "fr"]);
+
+ // Accepting the change shows the confirm message bar.
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing");
+ dialog.acceptDialog();
+ await dialogClosed;
+
+ await waitForMutation(
+ messageBar,
+ { attributes: true, attributeFilter: ["hidden"] },
+ target => !target.hidden
+ );
+
+ is(messageBar.hidden, false, "The message bar is now visible");
+ is(
+ messageBar.querySelector("button").getAttribute("locales"),
+ "he,en-US",
+ "The locales are set on the message bar button"
+ );
+
+ await Promise.all(addons.map(addon => addon.uninstall()));
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([
+ ["manage", "main", dialogId],
+
+ // Install id is not recorded since it was already installed.
+ ["add", "dialog", dialogId],
+ ["add", "dialog", dialogId],
+
+ ["remove", "dialog", dialogId],
+ ["remove", "dialog", dialogId],
+
+ ["add", "dialog", dialogId],
+ ["accept", "dialog", dialogId],
+ ]);
+});
+
+add_task(async function testInstallFromAMO() {
+ let langpacks = await AddonManager.getAddonsByTypes(["locale"]);
+ is(langpacks.length, 0, "There are no langpacks installed");
+
+ let langpacksFile = await createLanguageToolsFile();
+ let langpacksUrl = Services.io.newFileURI(langpacksFile).spec;
+ let dictionaryBrowseFile = await createDictionaryBrowseResults();
+ let browseApiEndpoint = Services.io.newFileURI(dictionaryBrowseFile).spec;
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", false],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ["intl.locale.requested", "en-US"],
+ ["extensions.getAddons.langpacks.url", langpacksUrl],
+ ["extensions.langpacks.signatures.required", false],
+ ["extensions.getAddons.get.url", browseApiEndpoint],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let messageBar = doc.getElementById("confirmBrowserLanguage");
+ is(messageBar.hidden, true, "The message bar is hidden at first");
+
+ // Verify only en-US is listed on the main pane.
+ let getMainPaneLocales = () => {
+ let available = doc.getElementById("primaryBrowserLocale");
+ let availableLocales = Array.from(available.menupopup.children);
+ return availableLocales
+ .map(item => item.value)
+ .sort()
+ .join(",");
+ };
+ is(getMainPaneLocales(), "en-US,search", "Only en-US installed to start");
+
+ // Open the dialog.
+ let { dialog, dialogDoc, available, selected } = await openDialog(doc, true);
+ let firstDialogId = getDialogId(dialogDoc);
+
+ // Make sure the message bar is still hidden.
+ is(
+ messageBar.hidden,
+ true,
+ "The message bar is still hidden after searching"
+ );
+
+ if (available.itemCount == 1) {
+ await waitForMutation(
+ available.menupopup,
+ { childList: true },
+ target => available.itemCount > 1
+ );
+ }
+
+ // The initial order is set by the pref.
+ assertLocaleOrder(selected, "en-US");
+ assertAvailableLocales(available, ["fr", "he", "pl"]);
+ is(
+ Services.locale.availableLocales.join(","),
+ "en-US",
+ "There is only one installed locale"
+ );
+
+ // Verify that there are no extra dictionaries.
+ let dicts = await AddonManager.getAddonsByTypes(["dictionary"]);
+ is(dicts.length, 0, "There are no installed dictionaries");
+
+ let installId = null;
+ AddonTestUtils.promiseInstallEvent("onInstallEnded").then(([install]) => {
+ installId = install.installId;
+ });
+
+ // Add Polish, this will install the langpack.
+ await selectLocale("pl", available, selected, dialogDoc);
+
+ ok(installId, "We got an installId for the langpack installation");
+
+ let langpack = await AddonManager.getAddonByID(langpackId("pl"));
+ Assert.deepEqual(
+ langpack.installTelemetryInfo,
+ { source: "about:preferences" },
+ "The source is set to preferences"
+ );
+
+ // Verify the list is correct.
+ assertLocaleOrder(selected, "pl,en-US");
+ assertAvailableLocales(available, ["fr", "he"]);
+ is(
+ Services.locale.availableLocales.sort().join(","),
+ "en-US,pl",
+ "Polish is now installed"
+ );
+
+ await BrowserTestUtils.waitForCondition(async () => {
+ let newDicts = await AddonManager.getAddonsByTypes(["dictionary"]);
+ let done = !!newDicts.length;
+
+ if (done) {
+ is(
+ newDicts[0].id,
+ DICTIONARY_ID_PL,
+ "The polish dictionary was installed"
+ );
+ }
+
+ return done;
+ });
+
+ // Move pl down the list, which prevents an error since it isn't valid.
+ dialogDoc.getElementById("down").doCommand();
+ assertLocaleOrder(selected, "en-US,pl");
+
+ // Test that disabling the langpack removes it from the list.
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing");
+ dialog.acceptDialog();
+ await dialogClosed;
+
+ // Verify pl is now available to select.
+ is(getMainPaneLocales(), "en-US,pl,search", "en-US and pl now available");
+
+ // Disable the Polish langpack.
+ langpack = await AddonManager.getAddonByID("langpack-pl@firefox.mozilla.org");
+ await langpack.disable();
+
+ ({ dialogDoc, available, selected } = await openDialog(doc, true));
+ let secondDialogId = getDialogId(dialogDoc);
+
+ // Wait for the available langpacks to load.
+ if (available.itemCount == 1) {
+ await waitForMutation(
+ available.menupopup,
+ { childList: true },
+ target => available.itemCount > 1
+ );
+ }
+ assertLocaleOrder(selected, "en-US");
+ assertAvailableLocales(available, ["fr", "he", "pl"]);
+
+ // Uninstall the langpack and dictionary.
+ let installs = await AddonManager.getAddonsByTypes(["locale", "dictionary"]);
+ is(installs.length, 2, "There is one langpack and one dictionary installed");
+ await Promise.all(installs.map(item => item.uninstall()));
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([
+ // First dialog installs a locale and accepts.
+ ["search", "main", firstDialogId],
+ // It has an installId since it was downloaded.
+ ["add", "dialog", firstDialogId, { installId }],
+ // It got moved down to avoid errors with finding translations.
+ ["reorder", "dialog", firstDialogId],
+ ["accept", "dialog", firstDialogId],
+
+ // The second dialog just checks the state and is closed with the tab.
+ ["search", "main", secondDialogId],
+ ["cancel", "dialog", secondDialogId],
+ ]);
+});
+
+let hasSearchOption = popup =>
+ Array.from(popup.children).some(el => el.value == "search");
+
+add_task(async function testDownloadEnabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", false],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+
+ let defaultMenulist = doc.getElementById("primaryBrowserLocale");
+ ok(
+ hasSearchOption(defaultMenulist.menupopup),
+ "There's a search option in the General pane"
+ );
+
+ let { available } = await openDialog(doc, false);
+ ok(
+ hasSearchOption(available.menupopup),
+ "There's a search option in the dialog"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testDownloadDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", false],
+ ["intl.multilingual.liveReload", false],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+
+ let defaultMenulist = doc.getElementById("primaryBrowserLocale");
+ ok(
+ !hasSearchOption(defaultMenulist.menupopup),
+ "There's no search option in the General pane"
+ );
+
+ let { available } = await openDialog(doc, false);
+ ok(
+ !hasSearchOption(available.menupopup),
+ "There's no search option in the dialog"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testReorderMainPane() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", false],
+ ["intl.multilingual.liveReload", false],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ["intl.locale.requested", "en-US"],
+ ["extensions.langpacks.signatures.required", false],
+ ],
+ });
+
+ // Clear the telemetry from other tests.
+ Services.telemetry.clearEvents();
+
+ let langpacks = await createTestLangpacks();
+ let addons = await Promise.all(
+ langpacks.map(async ([locale, file]) => {
+ let install = await AddonTestUtils.promiseInstallFile(file);
+ return install.addon;
+ })
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+
+ let messageBar = doc.getElementById("confirmBrowserLanguage");
+ is(messageBar.hidden, true, "The message bar is hidden at first");
+
+ let available = doc.getElementById("primaryBrowserLocale");
+ let availableLocales = Array.from(available.menupopup.children);
+ let availableCodes = availableLocales
+ .map(item => item.value)
+ .sort()
+ .join(",");
+ is(
+ availableCodes,
+ "en-US,fr,he,pl",
+ "All of the available locales are listed"
+ );
+
+ is(available.selectedItem.value, "en-US", "English is selected");
+
+ let hebrew = availableLocales.find(item => item.value == "he");
+ hebrew.click();
+ available.menupopup.hidePopup();
+
+ await BrowserTestUtils.waitForCondition(
+ () => !messageBar.hidden,
+ "Wait for message bar to show"
+ );
+
+ is(messageBar.hidden, false, "The message bar is now shown");
+ is(
+ messageBar.querySelector("button").getAttribute("locales"),
+ "he,en-US",
+ "The locales are set on the message bar button"
+ );
+
+ await Promise.all(addons.map(addon => addon.uninstall()));
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([["reorder", "main"]]);
+});
+
+add_task(async function testLiveLanguageReloading() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", true],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ["intl.locale.requested", "en-US,fr,he,de"],
+ ["extensions.langpacks.signatures.required", false],
+ ],
+ });
+
+ // Clear the telemetry from other tests.
+ Services.telemetry.clearEvents();
+
+ let langpacks = await createTestLangpacks();
+ let addons = await Promise.all(
+ langpacks.map(async ([locale, file]) => {
+ let install = await AddonTestUtils.promiseInstallFile(file);
+ return install.addon;
+ })
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+
+ let available = doc.getElementById("primaryBrowserLocale");
+ let availableLocales = Array.from(available.menupopup.children);
+
+ is(
+ Services.locale.appLocaleAsBCP47,
+ "en-US",
+ "The app locale starts as English."
+ );
+
+ Assert.deepEqual(
+ Services.locale.requestedLocales,
+ ["en-US", "fr", "he", "de"],
+ "The locale order starts as what was initially requested."
+ );
+
+ // French and English are both LTR languages.
+ let french = availableLocales.find(item => item.value == "fr");
+
+ french.click();
+ available.menupopup.hidePopup();
+
+ is(
+ Services.locale.appLocaleAsBCP47,
+ "fr",
+ "The app locale was changed to French"
+ );
+
+ Assert.deepEqual(
+ Services.locale.requestedLocales,
+ ["fr", "en-US", "he", "de"],
+ "The locale order is switched to french first."
+ );
+
+ await Promise.all(addons.map(addon => addon.uninstall()));
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([["reorder", "main"]]);
+});
+
+add_task(async function testLiveLanguageReloadingBidiOff() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", true],
+ ["intl.multilingual.liveReloadBidirectional", false],
+ ["intl.locale.requested", "en-US,fr,he,de"],
+ ["extensions.langpacks.signatures.required", false],
+ ],
+ });
+
+ // Clear the telemetry from other tests.
+ Services.telemetry.clearEvents();
+
+ let langpacks = await createTestLangpacks();
+ let addons = await Promise.all(
+ langpacks.map(async ([locale, file]) => {
+ let install = await AddonTestUtils.promiseInstallFile(file);
+ return install.addon;
+ })
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+
+ let available = doc.getElementById("primaryBrowserLocale");
+ let availableLocales = Array.from(available.menupopup.children);
+
+ is(
+ Services.locale.appLocaleAsBCP47,
+ "en-US",
+ "The app locale starts as English."
+ );
+
+ Assert.deepEqual(
+ Services.locale.requestedLocales,
+ ["en-US", "fr", "he", "de"],
+ "The locale order starts as what was initially requested."
+ );
+
+ let messageBar = doc.getElementById("confirmBrowserLanguage");
+ is(messageBar.hidden, true, "The message bar is hidden at first");
+
+ // English is LTR and Hebrew is RTL.
+ let hebrew = availableLocales.find(item => item.value == "he");
+
+ hebrew.click();
+ available.menupopup.hidePopup();
+
+ await BrowserTestUtils.waitForCondition(
+ () => !messageBar.hidden,
+ "Wait for message bar to show"
+ );
+
+ is(messageBar.hidden, false, "The message bar is now shown");
+
+ is(
+ Services.locale.appLocaleAsBCP47,
+ "en-US",
+ "The app locale remains in English"
+ );
+
+ Assert.deepEqual(
+ Services.locale.requestedLocales,
+ ["en-US", "fr", "he", "de"],
+ "The locale order did not change."
+ );
+
+ await Promise.all(addons.map(addon => addon.uninstall()));
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([["reorder", "main"]]);
+});
+
+add_task(async function testLiveLanguageReloadingBidiOn() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.multilingual.enabled", true],
+ ["intl.multilingual.downloadEnabled", true],
+ ["intl.multilingual.liveReload", true],
+ ["intl.multilingual.liveReloadBidirectional", true],
+ ["intl.locale.requested", "en-US,fr,he,de"],
+ ["extensions.langpacks.signatures.required", false],
+ ],
+ });
+
+ // Clear the telemetry from other tests.
+ Services.telemetry.clearEvents();
+
+ let langpacks = await createTestLangpacks();
+ let addons = await Promise.all(
+ langpacks.map(async ([locale, file]) => {
+ let install = await AddonTestUtils.promiseInstallFile(file);
+ return install.addon;
+ })
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+
+ let available = doc.getElementById("primaryBrowserLocale");
+ let availableLocales = Array.from(available.menupopup.children);
+
+ is(
+ Services.locale.appLocaleAsBCP47,
+ "en-US",
+ "The app locale starts as English."
+ );
+
+ Assert.deepEqual(
+ Services.locale.requestedLocales,
+ ["en-US", "fr", "he", "de"],
+ "The locale order starts as what was initially requested."
+ );
+
+ let messageBar = doc.getElementById("confirmBrowserLanguage");
+ is(messageBar.hidden, true, "The message bar is hidden at first");
+
+ // English is LTR and Hebrew is RTL.
+ let hebrew = availableLocales.find(item => item.value == "he");
+
+ hebrew.click();
+ available.menupopup.hidePopup();
+
+ is(messageBar.hidden, true, "The message bar is still hidden");
+
+ is(
+ Services.locale.appLocaleAsBCP47,
+ "he",
+ "The app locale was changed to Hebrew."
+ );
+
+ Assert.deepEqual(
+ Services.locale.requestedLocales,
+ ["he", "en-US", "fr", "de"],
+ "The locale changed with Hebrew first."
+ );
+
+ await Promise.all(addons.map(addon => addon.uninstall()));
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ assertTelemetryRecorded([["reorder", "main"]]);
+});
diff --git a/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js b/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js
new file mode 100644
index 0000000000..bc928656ab
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var originalWindowHeight;
+registerCleanupFunction(function () {
+ window.resizeTo(window.outerWidth, originalWindowHeight);
+ while (gBrowser.tabs[1]) {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ }
+});
+
+add_task(async function () {
+ originalWindowHeight = window.outerHeight;
+ window.resizeTo(window.outerWidth, 300);
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneSearch", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneSearch", "Search pane was selected");
+ let mainContent = gBrowser.contentDocument.querySelector(".main-content");
+ mainContent.scrollTop = 50;
+ is(mainContent.scrollTop, 50, "main-content should be scrolled 50 pixels");
+
+ await gBrowser.contentWindow.gotoPref("paneGeneral");
+
+ is(
+ mainContent.scrollTop,
+ 0,
+ "Switching to a different category should reset the scroll position"
+ );
+});
diff --git a/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js b/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js
new file mode 100644
index 0000000000..26e79b648d
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test opening to the differerent panes and subcategories in Preferences
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy");
+ is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected");
+ prefs = await openPreferencesViaHash("privacy");
+ is(
+ prefs.selectedPane,
+ "panePrivacy",
+ "Privacy pane is selected when hash is 'privacy'"
+ );
+ prefs = await openPreferencesViaOpenPreferencesAPI("nonexistant-category");
+ is(
+ prefs.selectedPane,
+ "paneGeneral",
+ "General pane is selected by default when a nonexistant-category is requested"
+ );
+ prefs = await openPreferencesViaHash("nonexistant-category");
+ is(
+ prefs.selectedPane,
+ "paneGeneral",
+ "General pane is selected when hash is a nonexistant-category"
+ );
+ prefs = await openPreferencesViaHash();
+ is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
+ prefs = await openPreferencesViaOpenPreferencesAPI("privacy-reports", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected by default");
+ let doc = gBrowser.contentDocument;
+ is(
+ doc.location.hash,
+ "#privacy",
+ "The subcategory should be removed from the URI"
+ );
+ await TestUtils.waitForCondition(
+ () => doc.querySelector(".spotlight"),
+ "Wait for the reports section is spotlighted."
+ );
+ is(
+ doc.querySelector(".spotlight").getAttribute("data-subcategory"),
+ "reports",
+ "The reports section is spotlighted."
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test opening Preferences with subcategory on an existing Preferences tab. See bug 1358475.
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("general", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
+ let doc = gBrowser.contentDocument;
+ is(
+ doc.location.hash,
+ "#general",
+ "The subcategory should be removed from the URI"
+ );
+ // The reasons that here just call the `openPreferences` API without the helping function are
+ // - already opened one about:preferences tab up there and
+ // - the goal is to test on the existing tab and
+ // - using `openPreferencesViaOpenPreferencesAPI` would introduce more handling of additional about:blank and unneccessary event
+ await openPreferences("privacy-reports");
+ let selectedPane = gBrowser.contentWindow.history.state;
+ is(selectedPane, "panePrivacy", "Privacy pane should be selected");
+ is(
+ doc.location.hash,
+ "#privacy",
+ "The subcategory should be removed from the URI"
+ );
+ await TestUtils.waitForCondition(
+ () => doc.querySelector(".spotlight"),
+ "Wait for the reports section is spotlighted."
+ );
+ is(
+ doc.querySelector(".spotlight").getAttribute("data-subcategory"),
+ "reports",
+ "The reports section is spotlighted."
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test opening to a subcategory displays the correct values for preferences
+add_task(async function () {
+ // Skip if crash reporting isn't enabled since the checkbox will be missing.
+ if (!AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", true]],
+ });
+ await openPreferencesViaOpenPreferencesAPI("privacy-reports", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ ok(
+ doc.querySelector("#automaticallySubmitCrashesBox").checked,
+ "Checkbox for automatically submitting crashes should be checked when the pref is true and only Reports are requested"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function () {
+ // Skip if crash reporting isn't enabled since the checkbox will be missing.
+ if (!AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", false]],
+ });
+ await openPreferencesViaOpenPreferencesAPI("privacy-reports", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ ok(
+ !doc.querySelector("#automaticallySubmitCrashesBox").checked,
+ "Checkbox for automatically submitting crashes should not be checked when the pref is false only Reports are requested"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SpecialPowers.popPrefEnv();
+});
+
+function openPreferencesViaHash(aPane) {
+ return new Promise(resolve => {
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:preferences" + (aPane ? "#" + aPane : "")
+ );
+ let newTabBrowser = gBrowser.selectedBrowser;
+
+ newTabBrowser.addEventListener(
+ "Initialized",
+ function () {
+ newTabBrowser.contentWindow.addEventListener(
+ "load",
+ async function () {
+ let win = gBrowser.contentWindow;
+ let selectedPane = win.history.state;
+ await finalPrefPaneLoaded;
+ gBrowser.removeCurrentTab();
+ resolve({ selectedPane });
+ },
+ { once: true }
+ );
+ },
+ { capture: true, once: true }
+ );
+ });
+}
diff --git a/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js
new file mode 100644
index 0000000000..ef387f9a9d
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js
@@ -0,0 +1,116 @@
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+add_task(async function () {
+ waitForExplicitFinish();
+
+ const tabURL =
+ getRootDirectory(gTestPath) +
+ "browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml";
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: tabURL },
+ async function (browser) {
+ let doc = browser.contentDocument;
+ let container = doc.getElementById("container");
+
+ // Test button
+ let button = doc.getElementById("button");
+ button.focus();
+ let initialScrollTop = container.scrollTop;
+ EventUtils.sendString(" ");
+ await checkPageScrolling(container, "button", initialScrollTop);
+
+ // Test checkbox
+ let checkbox = doc.getElementById("checkbox");
+ checkbox.focus();
+ initialScrollTop = container.scrollTop;
+ EventUtils.sendString(" ");
+ ok(checkbox.checked, "Checkbox is checked");
+ await checkPageScrolling(container, "checkbox", initialScrollTop);
+
+ // Test radio
+ let radiogroup = doc.getElementById("radiogroup");
+ radiogroup.focus();
+ initialScrollTop = container.scrollTop;
+ EventUtils.sendString(" ");
+ await checkPageScrolling(container, "radio", initialScrollTop);
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#search" },
+ async function (browser) {
+ let doc = browser.contentDocument;
+ let container = doc.getElementsByClassName("main-content")[0];
+
+ // Test search
+ let engineList = doc.getElementById("engineList");
+ engineList.focus();
+ let initialScrollTop = container.scrollTop;
+ EventUtils.sendString(" ");
+ is(
+ engineList.view.selection.currentIndex,
+ 0,
+ "Search engineList is selected"
+ );
+ EventUtils.sendString(" ");
+ await checkPageScrolling(
+ container,
+ "search engineList",
+ initialScrollTop
+ );
+ }
+ );
+
+ // Test session restore
+ const CRASH_URL = "about:mozilla";
+ const CRASH_FAVICON = "chrome://branding/content/icon32.png";
+ const CRASH_SHENTRY = { url: CRASH_URL };
+ const CRASH_TAB = { entries: [CRASH_SHENTRY], image: CRASH_FAVICON };
+ const CRASH_STATE = { windows: [{ tabs: [CRASH_TAB] }] };
+
+ const TAB_URL = "about:sessionrestore";
+ const TAB_FORMDATA = { url: TAB_URL, id: { sessionData: CRASH_STATE } };
+ const TAB_SHENTRY = { url: TAB_URL, triggeringPrincipal_base64 };
+ const TAB_STATE = { entries: [TAB_SHENTRY], formdata: TAB_FORMDATA };
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+
+ // Fake a post-crash tab
+ SessionStore.setTabState(tab, JSON.stringify(TAB_STATE));
+
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let doc = tab.linkedBrowser.contentDocument;
+
+ // Make body scrollable
+ doc.body.style.height = doc.body.clientHeight + 100 + "px";
+
+ let tabsToggle = doc.getElementById("tabsToggle");
+ tabsToggle.focus();
+ let initialScrollTop = doc.documentElement.scrollTop;
+ EventUtils.sendString(" ");
+ await checkPageScrolling(
+ doc.documentElement,
+ "session restore",
+ initialScrollTop
+ );
+
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function checkPageScrolling(container, type, initialScrollTop = 0) {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ is(
+ container.scrollTop,
+ initialScrollTop,
+ "Page should not scroll when " + type + " flipped"
+ );
+ resolve();
+ }, 0);
+ });
+}
diff --git a/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml
new file mode 100644
index 0000000000..8b3e39a39c
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+ XUL Widget Test for Bug 1184989
+ -->
+<window title="Bug 1184989 Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<vbox id="container" style="height: 200px; overflow: auto;">
+ <vbox style="height: 500px;">
+ <hbox>
+ <button id="button" label="button" />
+ </hbox>
+
+ <hbox>
+ <checkbox id="checkbox" label="checkbox" />
+ </hbox>
+
+ <hbox>
+ <radiogroup id="radiogroup">
+ <radio id="radio" label="radio" />
+ </radiogroup>
+ </hbox>
+ </vbox>
+</vbox>
+
+</window>
diff --git a/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js b/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js
new file mode 100644
index 0000000000..cd5b00b5f2
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ Services.prefs.lockPref("browser.download.useDownloadDir");
+
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ var downloadFolder = doc.getElementById("downloadFolder");
+ var chooseFolder = doc.getElementById("chooseFolder");
+ is(
+ downloadFolder.disabled,
+ false,
+ "Download folder field should not be disabled."
+ );
+ is(chooseFolder.disabled, false, "Choose folder should not be disabled.");
+
+ gBrowser.removeCurrentTab();
+
+ Services.prefs.unlockPref("browser.download.useDownloadDir");
+});
diff --git a/browser/components/preferences/tests/browser_bug1579418.js b/browser/components/preferences/tests/browser_bug1579418.js
new file mode 100644
index 0000000000..a179ae9936
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug1579418.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function default_homepage_test() {
+ let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage");
+ let oldStartpagePref = Services.prefs.getIntPref("browser.startup.page");
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+
+ let doc = gBrowser.contentDocument;
+ let homeMode = doc.getElementById("homeMode");
+ let customSettings = doc.getElementById("customSettings");
+
+ // HOME_MODE_FIREFOX_HOME
+ homeMode.value = 0;
+
+ homeMode.dispatchEvent(new Event("command"));
+
+ is(Services.prefs.getCharPref("browser.startup.homepage"), "about:home");
+
+ // HOME_MODE_BLANK
+ homeMode.value = 1;
+
+ homeMode.dispatchEvent(new Event("command"));
+
+ await TestUtils.waitForCondition(
+ () => customSettings.hidden === true,
+ "Wait for customSettings to be hidden."
+ );
+
+ is(
+ Services.prefs.getCharPref("browser.startup.homepage"),
+ "chrome://browser/content/blanktab.html"
+ );
+
+ // HOME_MODE_CUSTOM
+ homeMode.value = 2;
+
+ homeMode.dispatchEvent(new Event("command"));
+
+ await TestUtils.waitForCondition(
+ () => customSettings.hidden === false,
+ "Wait for customSettings to be shown."
+ );
+
+ is(customSettings.hidden, false, "homePageURL should be visible");
+
+ registerCleanupFunction(async () => {
+ Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref);
+ Services.prefs.setIntPref("browser.startup.page", oldStartpagePref);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
diff --git a/browser/components/preferences/tests/browser_bug410900.js b/browser/components/preferences/tests/browser_bug410900.js
new file mode 100644
index 0000000000..4fd4bb21f5
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug410900.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ // Setup a phony handler to ensure the app pane will be populated.
+ var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ handler.name = "App pane alive test";
+ handler.uriTemplate = "http://test.mozilla.org/%s";
+
+ var extps = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ var info = extps.getProtocolHandlerInfo("apppanetest");
+ info.possibleApplicationHandlers.appendElement(handler);
+
+ var hserv = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ hserv.store(info);
+
+ openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true })
+ .then(() => gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList)
+ .then(() => runTest(gBrowser.selectedBrowser.contentWindow));
+}
+
+function runTest(win) {
+ var rbox = win.document.getElementById("handlersView");
+ ok(rbox, "handlersView is present");
+
+ var items = rbox && rbox.getElementsByTagName("richlistitem");
+ ok(items && !!items.length, "App handler list populated");
+
+ var handlerAdded = false;
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].getAttribute("type") == "apppanetest") {
+ handlerAdded = true;
+ }
+ }
+ ok(handlerAdded, "apppanetest protocol handler was successfully added");
+
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/components/preferences/tests/browser_bug731866.js b/browser/components/preferences/tests/browser_bug731866.js
new file mode 100644
index 0000000000..b090535a49
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug731866.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const browserContainersGroupDisabled = !SpecialPowers.getBoolPref(
+ "privacy.userContext.ui.enabled"
+);
+const cookieBannerHandlingDisabled = !SpecialPowers.getBoolPref(
+ "cookiebanners.ui.desktop.enabled"
+);
+const updatePrefContainers = ["updatesCategory", "updateApp"];
+const updateContainersGroupDisabled =
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId");
+
+function test() {
+ waitForExplicitFinish();
+ open_preferences(runTest);
+}
+
+var gElements;
+
+function checkElements(expectedPane) {
+ for (let element of gElements) {
+ // keyset elements fail is_element_visible checks because they are never visible.
+ // special-case the drmGroup item because its visibility depends on pref + OS version
+ if (element.nodeName == "keyset" || element.id === "drmGroup") {
+ continue;
+ }
+
+ // The browserContainersGroup is still only pref-on on Nightly
+ if (
+ element.id == "browserContainersGroup" &&
+ browserContainersGroupDisabled
+ ) {
+ is_element_hidden(
+ element,
+ "Disabled browserContainersGroup should be hidden"
+ );
+ continue;
+ }
+
+ // Cookie Banner Handling is currently disabled by default (bug 1800679)
+ if (
+ element.id == "cookieBannerHandlingGroup" &&
+ cookieBannerHandlingDisabled
+ ) {
+ is_element_hidden(
+ element,
+ "Disabled cookieBannerHandlingGroup should be hidden"
+ );
+ continue;
+ }
+
+ // Update prefs are hidden when running an MSIX build
+ if (
+ updatePrefContainers.includes(element.id) &&
+ updateContainersGroupDisabled
+ ) {
+ is_element_hidden(element, "Disabled " + element + " should be hidden");
+ continue;
+ }
+
+ let attributeValue = element.getAttribute("data-category");
+ let suffix = " (id=" + element.id + ")";
+ if (attributeValue == "pane" + expectedPane) {
+ is_element_visible(
+ element,
+ expectedPane + " elements should be visible" + suffix
+ );
+ } else {
+ is_element_hidden(
+ element,
+ "Elements not in " + expectedPane + " should be hidden" + suffix
+ );
+ }
+ }
+}
+
+async function runTest(win) {
+ is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
+
+ let tab = win.document;
+ gElements = tab.getElementById("mainPrefPane").children;
+
+ let panes = ["General", "Search", "Privacy", "Sync"];
+
+ for (let pane of panes) {
+ await win.gotoPref("pane" + pane);
+ checkElements(pane);
+ }
+
+ gBrowser.removeCurrentTab();
+ win.close();
+ finish();
+}
diff --git a/browser/components/preferences/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/tests/browser_bug795764_cachedisabled.js
new file mode 100644
index 0000000000..97f5aaf48f
--- /dev/null
+++ b/browser/components/preferences/tests/browser_bug795764_cachedisabled.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ // Adding one fake site so that the SiteDataManager would run.
+ // Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager.
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://www.foo.com"
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "persistent-storage",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+ registerCleanupFunction(function () {
+ Services.perms.removeFromPrincipal(principal, "persistent-storage");
+ });
+
+ SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.ui.enabled", true]],
+ }).then(() => open_preferences(runTest));
+}
+
+async function runTest(win) {
+ is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
+
+ let tab = win.document;
+ let elements = tab.getElementById("mainPrefPane").children;
+
+ // Test if privacy pane is opened correctly
+ await win.gotoPref("panePrivacy");
+ for (let element of elements) {
+ let attributeValue = element.getAttribute("data-category");
+
+ // Ignore the cookie banner handling section, as it is currently preffed
+ // off by default (bug 1800679).
+ if (element.id === "cookieBannerHandlingGroup") {
+ continue;
+ }
+
+ if (attributeValue == "panePrivacy") {
+ is_element_visible(element, "HTTPSOnly should be visible");
+
+ is_element_visible(
+ element,
+ `Privacy element of id=${element.id} should be visible`
+ );
+ } else {
+ is_element_hidden(
+ element,
+ `Non-Privacy element of id=${element.id} should be hidden`
+ );
+ }
+ }
+
+ gBrowser.removeCurrentTab();
+ win.close();
+ finish();
+}
diff --git a/browser/components/preferences/tests/browser_cert_export.js b/browser/components/preferences/tests/browser_cert_export.js
new file mode 100644
index 0000000000..48769f84e6
--- /dev/null
+++ b/browser/components/preferences/tests/browser_cert_export.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+
+function createTemporarySaveDirectory() {
+ var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ info("create testsavedir!");
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ info("return from createTempSaveDir: " + saveDir.path);
+ return saveDir;
+}
+
+// Create the folder the certificates will be saved into.
+var destDir = createTemporarySaveDirectory();
+registerCleanupFunction(function () {
+ destDir.remove(true);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+function stringOrArrayEquals(actual, expected, message) {
+ is(
+ typeof actual,
+ typeof expected,
+ "actual, expected should have the same type"
+ );
+ if (typeof expected == "string") {
+ is(actual, expected, message);
+ } else {
+ is(actual.toString(), expected.toString(), message);
+ }
+}
+
+var dialogWin;
+var exportButton;
+var expectedCert;
+
+async function setupTest() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let certButton = gBrowser.selectedBrowser.contentDocument.getElementById(
+ "viewCertificatesButton"
+ );
+ certButton.scrollIntoView();
+ let certDialogLoaded = promiseLoadSubDialog(
+ "chrome://pippki/content/certManager.xhtml"
+ );
+ certButton.click();
+ dialogWin = await certDialogLoaded;
+ let doc = dialogWin.document;
+ doc.getElementById("certmanagertabs").selectedTab =
+ doc.getElementById("ca_tab");
+ let treeView = doc.getElementById("ca-tree").view;
+ // Select any which cert. Ignore parent rows (ie rows without certs):
+ for (let i = 0; i < treeView.rowCount; i++) {
+ treeView.selection.select(i);
+ dialogWin.getSelectedCerts();
+ let certs = dialogWin.selected_certs; // yuck... but this is how the dialog works.
+ if (certs && certs.length == 1 && certs[0]) {
+ expectedCert = certs[0];
+ // OK, we managed to select a cert!
+ break;
+ }
+ }
+
+ exportButton = doc.getElementById("ca_exportButton");
+ is(exportButton.disabled, false, "Should enable export button");
+}
+
+async function checkCertExportWorks(
+ exportType,
+ encoding,
+ expectedFileContents
+) {
+ MockFilePicker.displayDirectory = destDir;
+ var destFile = destDir.clone();
+ MockFilePicker.init(window);
+ MockFilePicker.filterIndex = exportType;
+ MockFilePicker.showCallback = function (fp) {
+ info("showCallback");
+ let fileName = fp.defaultString;
+ info("fileName: " + fileName);
+ destFile.append(fileName);
+ MockFilePicker.setFiles([destFile]);
+ info("done showCallback");
+ };
+ let finishedExporting = TestUtils.topicObserved("cert-export-finished");
+ exportButton.click();
+ await finishedExporting;
+ MockFilePicker.cleanup();
+ if (destFile && destFile.exists()) {
+ let contents;
+ if (encoding === "utf-8") {
+ contents = await IOUtils.readUTF8(destFile.path);
+ } else {
+ is(encoding, "", "expected either utf-8 or empty string for encoding");
+ contents = await IOUtils.read(destFile.path);
+ }
+ stringOrArrayEquals(
+ contents,
+ expectedFileContents,
+ "Should have written correct contents"
+ );
+ destFile.remove(false);
+ } else {
+ ok(false, "No cert saved!");
+ }
+}
+
+add_task(setupTest);
+
+add_task(async function checkCertPEMExportWorks() {
+ let expectedContents = dialogWin.getPEMString(expectedCert);
+ await checkCertExportWorks(0, /* 0 = PEM */ "utf-8", expectedContents);
+});
+
+add_task(async function checkCertPEMChainExportWorks() {
+ let expectedContents = dialogWin.getPEMString(expectedCert);
+ await checkCertExportWorks(
+ 1, // 1 = PEM chain, but the chain is of length 1
+ "utf-8",
+ expectedContents
+ );
+});
+
+add_task(async function checkCertDERExportWorks() {
+ let expectedContents = Uint8Array.from(expectedCert.getRawDER());
+ await checkCertExportWorks(2, /* 2 = DER */ "", expectedContents);
+});
+
+function stringToTypedArray(str) {
+ let arr = new Uint8Array(str.length);
+ for (let i = 0; i < arr.length; i++) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return arr;
+}
+
+add_task(async function checkCertPKCS7ExportWorks() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ let expectedContents = stringToTypedArray(certdb.asPKCS7Blob([expectedCert]));
+ await checkCertExportWorks(3, /* 3 = PKCS7 */ "", expectedContents);
+});
+
+add_task(async function checkCertPKCS7ChainExportWorks() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ let expectedContents = stringToTypedArray(certdb.asPKCS7Blob([expectedCert]));
+ await checkCertExportWorks(
+ 4, // 4 = PKCS7 chain, but the chain is of length 1
+ "",
+ expectedContents
+ );
+});
diff --git a/browser/components/preferences/tests/browser_change_app_handler.js b/browser/components/preferences/tests/browser_change_app_handler.js
new file mode 100644
index 0000000000..b4ca92592f
--- /dev/null
+++ b/browser/components/preferences/tests/browser_change_app_handler.js
@@ -0,0 +1,155 @@
+var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+SimpleTest.requestCompleteLog();
+
+function setupFakeHandler() {
+ let info = gMimeSvc.getFromTypeAndExtension("text/plain", "foo.txt");
+ ok(
+ info.possibleLocalHandlers.length,
+ "Should have at least one known handler"
+ );
+ let handler = info.possibleLocalHandlers.queryElementAt(
+ 0,
+ Ci.nsILocalHandlerApp
+ );
+
+ let infoToModify = gMimeSvc.getFromTypeAndExtension(
+ "text/x-test-handler",
+ null
+ );
+ infoToModify.possibleApplicationHandlers.appendElement(handler);
+
+ gHandlerSvc.store(infoToModify);
+}
+
+add_task(async function () {
+ setupFakeHandler();
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+ let win = gBrowser.selectedBrowser.contentWindow;
+
+ let container = win.document.getElementById("handlersView");
+ let ourItem = container.querySelector(
+ "richlistitem[type='text/x-test-handler']"
+ );
+ ok(ourItem, "handlersView is present");
+ ourItem.scrollIntoView();
+ container.selectItem(ourItem);
+ ok(ourItem.selected, "Should be able to select our item.");
+
+ let list = ourItem.querySelector(".actionsMenu");
+
+ let chooseItem = list.menupopup.querySelector(".choose-app-item");
+ let dialogLoadedPromise = promiseLoadSubDialog(
+ "chrome://global/content/appPicker.xhtml"
+ );
+ let cmdEvent = win.document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ win,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ 0
+ );
+ chooseItem.dispatchEvent(cmdEvent);
+
+ let dialog = await dialogLoadedPromise;
+ info("Dialog loaded");
+
+ let dialogDoc = dialog.document;
+ let dialogElement = dialogDoc.getElementById("app-picker");
+ let dialogList = dialogDoc.getElementById("app-picker-listbox");
+ dialogList.selectItem(dialogList.firstElementChild);
+ let selectedApp = dialogList.firstElementChild.handlerApp;
+ dialogElement.acceptDialog();
+
+ // Verify results are correct in mime service:
+ let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
+ ok(
+ mimeInfo.preferredApplicationHandler.equals(selectedApp),
+ "App should be set as preferred."
+ );
+
+ // Check that we display this result:
+ ok(list.selectedItem, "Should have a selected item");
+ ok(
+ mimeInfo.preferredApplicationHandler.equals(list.selectedItem.handlerApp),
+ "App should be visible as preferred item."
+ );
+
+ // Now try to 'manage' this list:
+ dialogLoadedPromise = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/applicationManager.xhtml"
+ );
+
+ let manageItem = list.menupopup.querySelector(".manage-app-item");
+ cmdEvent = win.document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ win,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ 0
+ );
+ manageItem.dispatchEvent(cmdEvent);
+
+ dialog = await dialogLoadedPromise;
+ info("Dialog loaded the second time");
+
+ dialogDoc = dialog.document;
+ dialogElement = dialogDoc.getElementById("appManager");
+ dialogList = dialogDoc.getElementById("appList");
+ let itemToRemove = dialogList.querySelector(
+ 'richlistitem > label[value="' + selectedApp.name + '"]'
+ ).parentNode;
+ dialogList.selectItem(itemToRemove);
+ let itemsBefore = dialogList.children.length;
+ dialogDoc.getElementById("remove").click();
+ ok(!itemToRemove.parentNode, "Item got removed from DOM");
+ is(dialogList.children.length, itemsBefore - 1, "Item got removed");
+ dialogElement.acceptDialog();
+
+ // Verify results are correct in mime service:
+ mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
+ ok(
+ !mimeInfo.preferredApplicationHandler,
+ "App should no longer be set as preferred."
+ );
+
+ // Check that we display this result:
+ ok(list.selectedItem, "Should have a selected item");
+ ok(
+ !list.selectedItem.handlerApp,
+ "No app should be visible as preferred item."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+registerCleanupFunction(function () {
+ let infoToModify = gMimeSvc.getFromTypeAndExtension(
+ "text/x-test-handler",
+ null
+ );
+ gHandlerSvc.remove(infoToModify);
+});
diff --git a/browser/components/preferences/tests/browser_checkspelling.js b/browser/components/preferences/tests/browser_checkspelling.js
new file mode 100644
index 0000000000..a7895b4201
--- /dev/null
+++ b/browser/components/preferences/tests/browser_checkspelling.js
@@ -0,0 +1,34 @@
+add_task(async function () {
+ SpecialPowers.pushPrefEnv({ set: [["layout.spellcheckDefault", 2]] });
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let checkbox = doc.querySelector("#checkSpelling");
+ is(
+ checkbox.checked,
+ Services.prefs.getIntPref("layout.spellcheckDefault") == 2,
+ "checkbox should represent pref value before clicking on checkbox"
+ );
+ ok(
+ checkbox.checked,
+ "checkbox should be checked before clicking on checkbox"
+ );
+
+ checkbox.click();
+
+ is(
+ checkbox.checked,
+ Services.prefs.getIntPref("layout.spellcheckDefault") == 2,
+ "checkbox should represent pref value after clicking on checkbox"
+ );
+ ok(
+ !checkbox.checked,
+ "checkbox should not be checked after clicking on checkbox"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_connection.js b/browser/components/preferences/tests/browser_connection.js
new file mode 100644
index 0000000000..01cdf571f2
--- /dev/null
+++ b/browser/components/preferences/tests/browser_connection.js
@@ -0,0 +1,145 @@
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ // network.proxy.type needs to be backed up and restored because mochitest
+ // changes this setting from the default
+ let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type");
+ registerCleanupFunction(function () {
+ Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType);
+ Services.prefs.clearUserPref("network.proxy.no_proxies_on");
+ // On accepting the dialog, we also write TRR values, so we need to clear
+ // them. They are tested separately in browser_privacy_dnsoverhttps.js.
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ });
+
+ let connectionURL =
+ "chrome://browser/content/preferences/dialogs/connection.xhtml";
+
+ /*
+ The connection dialog alone won't save onaccept since it uses type="child",
+ so it has to be opened as a sub dialog of the main pref tab.
+ Open the main tab here.
+ */
+ open_preferences(async function tabOpened(aContentWindow) {
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences",
+ "about:preferences loaded"
+ );
+ let dialog = await openAndLoadSubDialog(connectionURL);
+ let dialogElement = dialog.document.getElementById("ConnectionsDialog");
+ let dialogClosingPromise = BrowserTestUtils.waitForEvent(
+ dialogElement,
+ "dialogclosing"
+ );
+
+ ok(dialog, "connection window opened");
+ runConnectionTests(dialog);
+ dialogElement.acceptDialog();
+
+ let dialogClosingEvent = await dialogClosingPromise;
+ ok(dialogClosingEvent, "connection window closed");
+ // runConnectionTests will have changed this pref - make sure it was
+ // sanitized correctly when the dialog was accepted
+ is(
+ Services.prefs.getCharPref("network.proxy.no_proxies_on"),
+ ".a.com,.b.com,.c.com",
+ "no_proxies_on pref has correct value"
+ );
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+
+// run a bunch of tests on the window containing connection.xul
+function runConnectionTests(win) {
+ let doc = win.document;
+ let networkProxyNone = doc.getElementById("networkProxyNone");
+ let networkProxyNonePref = win.Preferences.get("network.proxy.no_proxies_on");
+ let networkProxyTypePref = win.Preferences.get("network.proxy.type");
+
+ // make sure the networkProxyNone textbox is formatted properly
+ is(networkProxyNone.localName, "textarea", "networkProxyNone is a textarea");
+ is(
+ networkProxyNone.getAttribute("rows"),
+ "2",
+ "networkProxyNone textbox has two rows"
+ );
+
+ // make sure manual proxy controls are disabled when the window is opened
+ let networkProxyHTTP = doc.getElementById("networkProxyHTTP");
+ is(networkProxyHTTP.disabled, true, "networkProxyHTTP textbox is disabled");
+
+ // check if sanitizing the given input for the no_proxies_on pref results in
+ // expected string
+ function testSanitize(input, expected, errorMessage) {
+ networkProxyNonePref.value = input;
+ win.gConnectionsDialog.sanitizeNoProxiesPref();
+ is(networkProxyNonePref.value, expected, errorMessage);
+ }
+
+ // change this pref so proxy exceptions are actually configurable
+ networkProxyTypePref.value = 1;
+ is(networkProxyNone.disabled, false, "networkProxyNone textbox is enabled");
+
+ testSanitize(".a.com", ".a.com", "sanitize doesn't mess up single filter");
+ testSanitize(
+ ".a.com, .b.com, .c.com",
+ ".a.com, .b.com, .c.com",
+ "sanitize doesn't mess up multiple comma/space sep filters"
+ );
+ testSanitize(
+ ".a.com\n.b.com",
+ ".a.com,.b.com",
+ "sanitize turns line break into comma"
+ );
+ testSanitize(
+ ".a.com,\n.b.com",
+ ".a.com,.b.com",
+ "sanitize doesn't add duplicate comma after comma"
+ );
+ testSanitize(
+ ".a.com\n,.b.com",
+ ".a.com,.b.com",
+ "sanitize doesn't add duplicate comma before comma"
+ );
+ testSanitize(
+ ".a.com,\n,.b.com",
+ ".a.com,,.b.com",
+ "sanitize doesn't add duplicate comma surrounded by commas"
+ );
+ testSanitize(
+ ".a.com, \n.b.com",
+ ".a.com, .b.com",
+ "sanitize doesn't add comma after comma/space"
+ );
+ testSanitize(
+ ".a.com\n .b.com",
+ ".a.com, .b.com",
+ "sanitize adds comma before space"
+ );
+ testSanitize(
+ ".a.com\n\n\n;;\n;\n.b.com",
+ ".a.com,.b.com",
+ "sanitize only adds one comma per substring of bad chars"
+ );
+ testSanitize(
+ ".a.com,,.b.com",
+ ".a.com,,.b.com",
+ "duplicate commas from user are untouched"
+ );
+ testSanitize(
+ ".a.com\n.b.com\n.c.com,\n.d.com,\n.e.com",
+ ".a.com,.b.com,.c.com,.d.com,.e.com",
+ "sanitize replaces things globally"
+ );
+
+ // will check that this was sanitized properly after window closes
+ networkProxyNonePref.value = ".a.com;.b.com\n.c.com";
+}
diff --git a/browser/components/preferences/tests/browser_connection_bug1445991.js b/browser/components/preferences/tests/browser_connection_bug1445991.js
new file mode 100644
index 0000000000..ecb5068a26
--- /dev/null
+++ b/browser/components/preferences/tests/browser_connection_bug1445991.js
@@ -0,0 +1,31 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the disabled status of the autoconfig Reload button when the proxy type
+// is autoconfig (network.proxy.type == 2).
+add_task(async function testAutoconfigReloadButton() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.proxy.type", 2],
+ ["network.proxy.autoconfig_url", "file:///nonexistent.pac"],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const connectionURL =
+ "chrome://browser/content/preferences/dialogs/connection.xhtml";
+ const promiseDialogLoaded = promiseLoadSubDialog(connectionURL);
+ gBrowser.contentDocument.getElementById("connectionSettings").click();
+ const dialog = await promiseDialogLoaded;
+
+ ok(
+ !dialog.document.getElementById("autoReload").disabled,
+ "Reload button is enabled when proxy type is autoconfig"
+ );
+
+ dialog.close();
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_connection_bug1505330.js b/browser/components/preferences/tests/browser_connection_bug1505330.js
new file mode 100644
index 0000000000..94dfd4706e
--- /dev/null
+++ b/browser/components/preferences/tests/browser_connection_bug1505330.js
@@ -0,0 +1,31 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the disabled status of the autoconfig Reload button when the proxy type
+// is autoconfig (network.proxy.type == 2).
+add_task(async function testAutoconfigReloadButton() {
+ Services.prefs.lockPref("signon.autologin.proxy");
+
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const connectionURL =
+ "chrome://browser/content/preferences/dialogs/connection.xhtml";
+ const promiseDialogLoaded = promiseLoadSubDialog(connectionURL);
+ gBrowser.contentDocument.getElementById("connectionSettings").click();
+ const dialog = await promiseDialogLoaded;
+
+ ok(
+ !dialog.document.getElementById("networkProxyType").firstChild.disabled,
+ "Connection options should not be disabled"
+ );
+ ok(
+ dialog.document.getElementById("autologinProxy").disabled,
+ "Proxy autologin should be disabled"
+ );
+
+ dialog.close();
+ Services.prefs.unlockPref("signon.autologin.proxy");
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_connection_bug388287.js b/browser/components/preferences/tests/browser_connection_bug388287.js
new file mode 100644
index 0000000000..d6f0c3c9d0
--- /dev/null
+++ b/browser/components/preferences/tests/browser_connection_bug388287.js
@@ -0,0 +1,124 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+ const connectionURL =
+ "chrome://browser/content/preferences/dialogs/connection.xhtml";
+ let closeable = false;
+ let finalTest = false;
+
+ // The changed preferences need to be backed up and restored because this mochitest
+ // changes them setting from the default
+ let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type");
+ registerCleanupFunction(function () {
+ Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType);
+ Services.prefs.clearUserPref("network.proxy.share_proxy_settings");
+ for (let proxyType of ["http", "ssl", "socks"]) {
+ Services.prefs.clearUserPref("network.proxy." + proxyType);
+ Services.prefs.clearUserPref("network.proxy." + proxyType + "_port");
+ if (proxyType == "http") {
+ continue;
+ }
+ Services.prefs.clearUserPref("network.proxy.backup." + proxyType);
+ Services.prefs.clearUserPref(
+ "network.proxy.backup." + proxyType + "_port"
+ );
+ }
+ // On accepting the dialog, we also write TRR values, so we need to clear
+ // them. They are tested separately in browser_privacy_dnsoverhttps.js.
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ });
+
+ /*
+ The connection dialog alone won't save onaccept since it uses type="child",
+ so it has to be opened as a sub dialog of the main pref tab.
+ Open the main tab here.
+ */
+ open_preferences(async function tabOpened(aContentWindow) {
+ let dialog, dialogClosingPromise, dialogElement;
+ let proxyTypePref, sharePref, httpPref, httpPortPref;
+
+ // Convenient function to reset the variables for the new window
+ async function setDoc() {
+ if (closeable) {
+ let dialogClosingEvent = await dialogClosingPromise;
+ ok(dialogClosingEvent, "Connection dialog closed");
+ }
+
+ if (finalTest) {
+ gBrowser.removeCurrentTab();
+ finish();
+ return;
+ }
+
+ dialog = await openAndLoadSubDialog(connectionURL);
+ dialogElement = dialog.document.getElementById("ConnectionsDialog");
+ dialogClosingPromise = BrowserTestUtils.waitForEvent(
+ dialogElement,
+ "dialogclosing"
+ );
+
+ proxyTypePref = dialog.Preferences.get("network.proxy.type");
+ sharePref = dialog.Preferences.get("network.proxy.share_proxy_settings");
+ httpPref = dialog.Preferences.get("network.proxy.http");
+ httpPortPref = dialog.Preferences.get("network.proxy.http_port");
+ }
+
+ // This batch of tests should not close the dialog
+ await setDoc();
+
+ // Testing HTTP port 0 with share on
+ proxyTypePref.value = 1;
+ sharePref.value = true;
+ httpPref.value = "localhost";
+ httpPortPref.value = 0;
+ dialogElement.acceptDialog();
+
+ // Testing HTTP port 0 + FTP port 80 with share off
+ sharePref.value = false;
+ dialogElement.acceptDialog();
+
+ // Testing HTTP port 80 + FTP port 0 with share off
+ httpPortPref.value = 80;
+ dialogElement.acceptDialog();
+
+ // From now on, the dialog should close since we are giving it legitimate inputs.
+ // The test will timeout if the onbeforeaccept kicks in erroneously.
+ closeable = true;
+
+ // Both ports 80, share on
+ httpPortPref.value = 80;
+ dialogElement.acceptDialog();
+
+ // HTTP 80, FTP 0, with share on
+ await setDoc();
+ proxyTypePref.value = 1;
+ sharePref.value = true;
+ httpPref.value = "localhost";
+ httpPortPref.value = 80;
+ dialogElement.acceptDialog();
+
+ // HTTP host empty, port 0 with share on
+ await setDoc();
+ proxyTypePref.value = 1;
+ sharePref.value = true;
+ httpPref.value = "";
+ httpPortPref.value = 0;
+ dialogElement.acceptDialog();
+
+ // HTTP 0, but in no proxy mode
+ await setDoc();
+ proxyTypePref.value = 0;
+ sharePref.value = true;
+ httpPref.value = "localhost";
+ httpPortPref.value = 0;
+
+ // This is the final test, don't spawn another connection window
+ finalTest = true;
+ dialogElement.acceptDialog();
+ await setDoc();
+ });
+}
diff --git a/browser/components/preferences/tests/browser_containers_name_input.js b/browser/components/preferences/tests/browser_containers_name_input.js
new file mode 100644
index 0000000000..38785d3cb0
--- /dev/null
+++ b/browser/components/preferences/tests/browser_containers_name_input.js
@@ -0,0 +1,72 @@
+const CONTAINERS_URL =
+ "chrome://browser/content/preferences/dialogs/containers.xhtml";
+
+add_setup(async function () {
+ await openPreferencesViaOpenPreferencesAPI("containers", { leaveOpen: true });
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function () {
+ async function openDialog() {
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ let dialogPromise = promiseLoadSubDialog(CONTAINERS_URL);
+
+ let addButton = doc.getElementById("containersAdd");
+ addButton.doCommand();
+
+ let dialog = await dialogPromise;
+
+ return dialog.document;
+ }
+
+ let { contentDocument } = gBrowser.selectedBrowser;
+ let containerNodes = Array.from(
+ contentDocument.querySelectorAll("[data-category=paneContainers]")
+ );
+ ok(
+ containerNodes.find(node => node.getBoundingClientRect().width > 0),
+ "Should actually be showing the container nodes."
+ );
+
+ let doc = await openDialog();
+
+ let name = doc.getElementById("name");
+ let btnApplyChanges = doc.querySelector("dialog").getButton("accept");
+
+ Assert.equal(name.value, "", "The name textbox should initlally be empty");
+ Assert.ok(
+ btnApplyChanges.disabled,
+ "The done button should initially be disabled"
+ );
+
+ function setName(value) {
+ name.value = value;
+
+ let event = new doc.defaultView.InputEvent("input", { data: value });
+ SpecialPowers.dispatchEvent(doc.defaultView, name, event);
+ }
+
+ setName("test");
+
+ Assert.ok(
+ !btnApplyChanges.disabled,
+ "The done button should be enabled when the value is not empty"
+ );
+
+ setName("");
+
+ Assert.ok(
+ btnApplyChanges.disabled,
+ "The done button should be disabled when the value is empty"
+ );
+
+ setName("\u0009\u000B\u000C\u0020\u00A0\uFEFF\u000A\u000D\u2028\u2029");
+
+ Assert.ok(
+ btnApplyChanges.disabled,
+ "The done button should be disabled when the value contains only whitespaces"
+ );
+});
diff --git a/browser/components/preferences/tests/browser_contentblocking.js b/browser/components/preferences/tests/browser_contentblocking.js
new file mode 100644
index 0000000000..19bd45153b
--- /dev/null
+++ b/browser/components/preferences/tests/browser_contentblocking.js
@@ -0,0 +1,1382 @@
+/* eslint-env webextensions */
+
+"use strict";
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled";
+const NCB_PREF = "network.cookie.cookieBehavior";
+const NCBP_PREF = "network.cookie.cookieBehavior.pbmode";
+const CAT_PREF = "browser.contentblocking.category";
+const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+const STP_PREF = "privacy.trackingprotection.socialtracking.enabled";
+const CM_PREF = "privacy.trackingprotection.cryptomining.enabled";
+const EMAIL_TP_PREF = "privacy.trackingprotection.emailtracking.enabled";
+const EMAIL_TP_PBM_PREF =
+ "privacy.trackingprotection.emailtracking.pbmode.enabled";
+const LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled";
+const REFERRER_PREF = "network.http.referer.disallowCrossSiteRelaxingDefault";
+const REFERRER_TOP_PREF =
+ "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation";
+const OCSP_PREF = "privacy.partition.network_state.ocsp_cache";
+const QUERY_PARAM_STRIP_PREF = "privacy.query_stripping.enabled";
+const QUERY_PARAM_STRIP_PBM_PREF = "privacy.query_stripping.enabled.pbmode";
+const PREF_TEST_NOTIFICATIONS =
+ "browser.safebrowsing.test-notifications.enabled";
+const STRICT_PREF = "browser.contentblocking.features.strict";
+const PRIVACY_PAGE = "about:preferences#privacy";
+const ISOLATE_UI_PREF =
+ "browser.contentblocking.reject-and-isolate-cookies.preferences.ui.enabled";
+const FPI_PREF = "privacy.firstparty.isolate";
+
+const { EnterprisePolicyTesting, PoliciesPrefTracker } =
+ ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+
+requestLongerTimeout(2);
+
+add_task(async function testListUpdate() {
+ SpecialPowers.pushPrefEnv({ set: [[PREF_TEST_NOTIFICATIONS, true]] });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let fingerprintersCheckbox = doc.getElementById(
+ "contentBlockingFingerprintersCheckbox"
+ );
+ let updateObserved = TestUtils.topicObserved("safebrowsing-update-attempt");
+ fingerprintersCheckbox.click();
+ let url = (await updateObserved)[1];
+
+ ok(true, "Has tried to update after the fingerprinting checkbox was toggled");
+ is(
+ url,
+ "http://127.0.0.1:8888/safebrowsing-dummy/update",
+ "Using the correct list url to update"
+ );
+
+ let cryptominersCheckbox = doc.getElementById(
+ "contentBlockingCryptominersCheckbox"
+ );
+ updateObserved = TestUtils.topicObserved("safebrowsing-update-attempt");
+ cryptominersCheckbox.click();
+ url = (await updateObserved)[1];
+
+ ok(true, "Has tried to update after the cryptomining checkbox was toggled");
+ is(
+ url,
+ "http://127.0.0.1:8888/safebrowsing-dummy/update",
+ "Using the correct list url to update"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that the content blocking main category checkboxes have the correct default state.
+add_task(async function testContentBlockingMainCategory() {
+ let prefs = [
+ [TP_PREF, false],
+ [TP_PBM_PREF, true],
+ [STP_PREF, false],
+ [NCB_PREF, Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER],
+ [
+ NCBP_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [ISOLATE_UI_PREF, true],
+ [FPI_PREF, false],
+ ];
+
+ for (let pref of prefs) {
+ switch (typeof pref[1]) {
+ case "boolean":
+ SpecialPowers.setBoolPref(pref[0], pref[1]);
+ break;
+ case "number":
+ SpecialPowers.setIntPref(pref[0], pref[1]);
+ break;
+ }
+ }
+
+ let checkboxes = [
+ "#contentBlockingTrackingProtectionCheckbox",
+ "#contentBlockingBlockCookiesCheckbox",
+ ];
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ for (let selector of checkboxes) {
+ let element = doc.querySelector(selector);
+ ok(element, "checkbox " + selector + " exists");
+ is(
+ element.getAttribute("checked"),
+ "true",
+ "checkbox " + selector + " is checked"
+ );
+ }
+
+ // Ensure the dependent controls of the tracking protection subsection behave properly.
+ let tpCheckbox = doc.querySelector(checkboxes[0]);
+
+ let dependentControls = ["#trackingProtectionMenu"];
+ let alwaysEnabledControls = [
+ "#trackingProtectionMenuDesc",
+ ".content-blocking-category-name",
+ "#changeBlockListLink",
+ ];
+
+ tpCheckbox.checked = true;
+
+ // Select "Always" under "All Detected Trackers".
+ let menu = doc.querySelector("#trackingProtectionMenu");
+ let always = doc.querySelector(
+ "#trackingProtectionMenu > menupopup > menuitem[value=always]"
+ );
+ let privateElement = doc.querySelector(
+ "#trackingProtectionMenu > menupopup > menuitem[value=private]"
+ );
+ menu.selectedItem = always;
+ ok(
+ !privateElement.selected,
+ "The Only in private windows item should not be selected"
+ );
+ ok(always.selected, "The Always item should be selected");
+
+ // The first time, privacy-pane-tp-ui-updated won't be dispatched since the
+ // assignment above is a no-op.
+
+ // Ensure the dependent controls are enabled
+ checkControlState(doc, dependentControls, true);
+ checkControlState(doc, alwaysEnabledControls, true);
+
+ let promise = TestUtils.topicObserved("privacy-pane-tp-ui-updated");
+ tpCheckbox.click();
+
+ await promise;
+ ok(!tpCheckbox.checked, "The checkbox should now be unchecked");
+
+ // Ensure the dependent controls are disabled
+ checkControlState(doc, dependentControls, false);
+ checkControlState(doc, alwaysEnabledControls, true);
+
+ // Make sure the selection in the tracking protection submenu persists after
+ // a few times of checking and unchecking All Detected Trackers.
+ // Doing this in a loop in order to avoid typing in the unrolled version manually.
+ // We need to go from the checked state of the checkbox to unchecked back to
+ // checked again...
+ for (let i = 0; i < 3; ++i) {
+ promise = TestUtils.topicObserved("privacy-pane-tp-ui-updated");
+ tpCheckbox.click();
+
+ await promise;
+ is(tpCheckbox.checked, i % 2 == 0, "The checkbox should now be unchecked");
+ is(
+ privateElement.selected,
+ i % 2 == 0,
+ "The Only in private windows item should be selected by default, when the checkbox is checked"
+ );
+ ok(!always.selected, "The Always item should no longer be selected");
+ }
+
+ let cookieMenu = doc.querySelector("#blockCookiesMenu");
+ let cookieMenuTrackers = cookieMenu.querySelector(
+ "menupopup > menuitem[value=trackers]"
+ );
+ let cookieMenuTrackersPlusIsolate = cookieMenu.querySelector(
+ "menupopup > menuitem[value=trackers-plus-isolate]"
+ );
+ let cookieMenuUnvisited = cookieMenu.querySelector(
+ "menupopup > menuitem[value=unvisited]"
+ );
+ let cookieMenuAllThirdParties = doc.querySelector(
+ "menupopup > menuitem[value=all-third-parties]"
+ );
+ let cookieMenuAll = cookieMenu.querySelector(
+ "menupopup > menuitem[value=always]"
+ );
+ // Select block trackers
+ cookieMenuTrackers.click();
+ ok(cookieMenuTrackers.selected, "The trackers item should be selected");
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}`
+ );
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+ // Select block trackers and isolate
+ cookieMenuTrackersPlusIsolate.click();
+ ok(
+ cookieMenuTrackersPlusIsolate.selected,
+ "The trackers plus isolate item should be selected"
+ );
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+ // Select block unvisited
+ cookieMenuUnvisited.click();
+ ok(cookieMenuUnvisited.selected, "The unvisited item should be selected");
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}`
+ );
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+ // Select block all third party
+ cookieMenuAllThirdParties.click();
+ ok(
+ cookieMenuAllThirdParties.selected,
+ "The all-third-parties item should be selected"
+ );
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}`
+ );
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+ // Select block all third party
+ cookieMenuAll.click();
+ ok(cookieMenuAll.selected, "The all cookies item should be selected");
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}`
+ );
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+
+ gBrowser.removeCurrentTab();
+
+ // Ensure the block-trackers-plus-isolate option only shows in the dropdown if the UI pref is set.
+ Services.prefs.setBoolPref(ISOLATE_UI_PREF, false);
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ doc = gBrowser.contentDocument;
+ cookieMenuTrackersPlusIsolate = doc.querySelector(
+ "#blockCookiesMenu menupopup > menuitem[value=trackers-plus-isolate]"
+ );
+ ok(
+ cookieMenuTrackersPlusIsolate.hidden,
+ "Trackers plus isolate option is hidden from the dropdown if the ui pref is not set."
+ );
+
+ gBrowser.removeCurrentTab();
+
+ // Ensure the block-trackers-plus-isolate option only shows in the dropdown if FPI is disabled.
+ SpecialPowers.setIntPref(
+ NCB_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+ SpecialPowers.setBoolPref(FPI_PREF, true);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ doc = gBrowser.contentDocument;
+ cookieMenuTrackers = doc.querySelector(
+ "#blockCookiesMenu menupopup > menuitem[value=trackers]"
+ );
+ cookieMenuTrackersPlusIsolate = doc.querySelector(
+ "#blockCookiesMenu menupopup > menuitem[value=trackers-plus-isolate]"
+ );
+ ok(cookieMenuTrackers.selected, "The trackers item should be selected");
+ ok(
+ cookieMenuTrackersPlusIsolate.hidden,
+ "Trackers plus isolate option is hidden from the dropdown if the FPI pref is set."
+ );
+ gBrowser.removeCurrentTab();
+
+ for (let pref of prefs) {
+ SpecialPowers.clearUserPref(pref[0]);
+ }
+});
+
+// Tests that the content blocking "Standard" category radio sets the prefs to their default values.
+add_task(async function testContentBlockingStandardCategory() {
+ let prefs = {
+ [TP_PREF]: null,
+ [TP_PBM_PREF]: null,
+ [NCB_PREF]: null,
+ [NCBP_PREF]: null,
+ [FP_PREF]: null,
+ [STP_PREF]: null,
+ [CM_PREF]: null,
+ [EMAIL_TP_PREF]: null,
+ [EMAIL_TP_PBM_PREF]: null,
+ [LEVEL2_PREF]: null,
+ [REFERRER_PREF]: null,
+ [REFERRER_TOP_PREF]: null,
+ [OCSP_PREF]: null,
+ [QUERY_PARAM_STRIP_PREF]: null,
+ [QUERY_PARAM_STRIP_PBM_PREF]: null,
+ };
+
+ for (let pref in prefs) {
+ Services.prefs.clearUserPref(pref);
+ switch (Services.prefs.getPrefType(pref)) {
+ case Services.prefs.PREF_BOOL:
+ prefs[pref] = Services.prefs.getBoolPref(pref);
+ break;
+ case Services.prefs.PREF_INT:
+ prefs[pref] = Services.prefs.getIntPref(pref);
+ break;
+ case Services.prefs.PREF_STRING:
+ prefs[pref] = Services.prefs.getCharPref(pref);
+ break;
+ default:
+ ok(false, `Unknown pref type for ${pref}`);
+ }
+ }
+
+ Services.prefs.setBoolPref(TP_PREF, true);
+ Services.prefs.setBoolPref(TP_PBM_PREF, false);
+ Services.prefs.setIntPref(
+ NCB_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ Services.prefs.setIntPref(
+ NCBP_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ Services.prefs.setBoolPref(STP_PREF, !Services.prefs.getBoolPref(STP_PREF));
+ Services.prefs.setBoolPref(FP_PREF, !Services.prefs.getBoolPref(FP_PREF));
+ Services.prefs.setBoolPref(CM_PREF, !Services.prefs.getBoolPref(CM_PREF));
+ Services.prefs.setBoolPref(
+ EMAIL_TP_PREF,
+ !Services.prefs.getBoolPref(EMAIL_TP_PREF)
+ );
+ Services.prefs.setBoolPref(
+ EMAIL_TP_PBM_PREF,
+ !Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF)
+ );
+ Services.prefs.setBoolPref(
+ LEVEL2_PREF,
+ !Services.prefs.getBoolPref(LEVEL2_PREF)
+ );
+ Services.prefs.setBoolPref(
+ REFERRER_PREF,
+ !Services.prefs.getBoolPref(REFERRER_PREF)
+ );
+ Services.prefs.setBoolPref(
+ REFERRER_TOP_PREF,
+ !Services.prefs.getBoolPref(REFERRER_TOP_PREF)
+ );
+ Services.prefs.setBoolPref(OCSP_PREF, !Services.prefs.getBoolPref(OCSP_PREF));
+ Services.prefs.setBoolPref(
+ QUERY_PARAM_STRIP_PREF,
+ !Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF)
+ );
+ Services.prefs.setBoolPref(
+ QUERY_PARAM_STRIP_PBM_PREF,
+ !Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF)
+ );
+
+ for (let pref in prefs) {
+ switch (Services.prefs.getPrefType(pref)) {
+ case Services.prefs.PREF_BOOL:
+ // Account for prefs that may have retained their default value
+ if (Services.prefs.getBoolPref(pref) != prefs[pref]) {
+ ok(
+ Services.prefs.prefHasUserValue(pref),
+ `modified the pref ${pref}`
+ );
+ }
+ break;
+ case Services.prefs.PREF_INT:
+ if (Services.prefs.getIntPref(pref) != prefs[pref]) {
+ ok(
+ Services.prefs.prefHasUserValue(pref),
+ `modified the pref ${pref}`
+ );
+ }
+ break;
+ case Services.prefs.PREF_STRING:
+ if (Services.prefs.getCharPref(pref) != prefs[pref]) {
+ ok(
+ Services.prefs.prefHasUserValue(pref),
+ `modified the pref ${pref}`
+ );
+ }
+ break;
+ default:
+ ok(false, `Unknown pref type for ${pref}`);
+ }
+ }
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let standardRadioOption = doc.getElementById("standardRadio");
+ standardRadioOption.click();
+
+ // TP prefs are reset async to check for extensions controlling them.
+ await TestUtils.waitForCondition(
+ () => !Services.prefs.prefHasUserValue(TP_PREF)
+ );
+
+ for (let pref in prefs) {
+ ok(!Services.prefs.prefHasUserValue(pref), `reset the pref ${pref}`);
+ }
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "standard",
+ `${CAT_PREF} has been set to standard`
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that the content blocking "Strict" category radio sets the prefs to the expected values.
+add_task(async function testContentBlockingStrictCategory() {
+ Services.prefs.setBoolPref(TP_PREF, false);
+ Services.prefs.setBoolPref(TP_PBM_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_TP_PREF, false);
+ Services.prefs.setBoolPref(EMAIL_TP_PBM_PREF, false);
+ Services.prefs.setBoolPref(LEVEL2_PREF, false);
+ Services.prefs.setBoolPref(REFERRER_PREF, false);
+ Services.prefs.setBoolPref(REFERRER_TOP_PREF, false);
+ Services.prefs.setBoolPref(OCSP_PREF, false);
+ Services.prefs.setBoolPref(QUERY_PARAM_STRIP_PREF, false);
+ Services.prefs.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, false);
+ Services.prefs.setIntPref(
+ NCB_PREF,
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN
+ );
+ Services.prefs.setIntPref(
+ NCBP_PREF,
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN
+ );
+ let strict_pref = Services.prefs.getStringPref(STRICT_PREF).split(",");
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let strictRadioOption = doc.getElementById("strictRadio");
+ strictRadioOption.click();
+
+ // TP prefs are reset async to check for extensions controlling them.
+ await TestUtils.waitForCondition(
+ () => Services.prefs.getStringPref(CAT_PREF) == "strict"
+ );
+ // Depending on the definition of the STRICT_PREF, the dependant prefs may have been
+ // set to varying values. Ensure they have been set according to this definition.
+ for (let pref of strict_pref) {
+ switch (pref) {
+ case "tp":
+ is(
+ Services.prefs.getBoolPref(TP_PREF),
+ true,
+ `${TP_PREF} has been set to true`
+ );
+ break;
+ case "-tp":
+ is(
+ Services.prefs.getBoolPref(TP_PREF),
+ false,
+ `${TP_PREF} has been set to false`
+ );
+ break;
+ case "tpPrivate":
+ is(
+ Services.prefs.getBoolPref(TP_PBM_PREF),
+ true,
+ `${TP_PBM_PREF} has been set to true`
+ );
+ break;
+ case "-tpPrivate":
+ is(
+ Services.prefs.getBoolPref(TP_PBM_PREF),
+ false,
+ `${TP_PBM_PREF} has been set to false`
+ );
+ break;
+ case "fp":
+ is(
+ Services.prefs.getBoolPref(FP_PREF),
+ true,
+ `${FP_PREF} has been set to true`
+ );
+ break;
+ case "-fp":
+ is(
+ Services.prefs.getBoolPref(FP_PREF),
+ false,
+ `${FP_PREF} has been set to false`
+ );
+ break;
+ case "stp":
+ is(
+ Services.prefs.getBoolPref(STP_PREF),
+ true,
+ `${STP_PREF} has been set to true`
+ );
+ break;
+ case "-stp":
+ is(
+ Services.prefs.getBoolPref(STP_PREF),
+ false,
+ `${STP_PREF} has been set to false`
+ );
+ break;
+ case "cm":
+ is(
+ Services.prefs.getBoolPref(CM_PREF),
+ true,
+ `${CM_PREF} has been set to true`
+ );
+ break;
+ case "-cm":
+ is(
+ Services.prefs.getBoolPref(CM_PREF),
+ false,
+ `${CM_PREF} has been set to false`
+ );
+ break;
+ case "emailTP":
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PREF),
+ true,
+ `${EMAIL_TP_PREF} has been set to true`
+ );
+ break;
+ case "-emailTP":
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PREF),
+ false,
+ `${EMAIL_TP_PREF} has been set to false`
+ );
+ break;
+ case "emailTPPrivate":
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF),
+ true,
+ `${EMAIL_TP_PBM_PREF} has been set to true`
+ );
+ break;
+ case "-emailTPPrivate":
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF),
+ false,
+ `${EMAIL_TP_PBM_PREF} has been set to false`
+ );
+ break;
+ case "lvl2":
+ is(
+ Services.prefs.getBoolPref(LEVEL2_PREF),
+ true,
+ `${CM_PREF} has been set to true`
+ );
+ break;
+ case "-lvl2":
+ is(
+ Services.prefs.getBoolPref(LEVEL2_PREF),
+ false,
+ `${CM_PREF} has been set to false`
+ );
+ break;
+ case "rp":
+ is(
+ Services.prefs.getBoolPref(REFERRER_PREF),
+ true,
+ `${REFERRER_PREF} has been set to true`
+ );
+ break;
+ case "-rp":
+ is(
+ Services.prefs.getBoolPref(REFERRER_PREF),
+ false,
+ `${REFERRER_PREF} has been set to false`
+ );
+ break;
+ case "rpTop":
+ is(
+ Services.prefs.getBoolPref(REFERRER_TOP_PREF),
+ true,
+ `${REFERRER_TOP_PREF} has been set to true`
+ );
+ break;
+ case "-rpTop":
+ is(
+ Services.prefs.getBoolPref(REFERRER_TOP_PREF),
+ false,
+ `${REFERRER_TOP_PREF} has been set to false`
+ );
+ break;
+ case "ocsp":
+ is(
+ Services.prefs.getBoolPref(OCSP_PREF),
+ true,
+ `${OCSP_PREF} has been set to true`
+ );
+ break;
+ case "-ocsp":
+ is(
+ Services.prefs.getBoolPref(OCSP_PREF),
+ false,
+ `${OCSP_PREF} has been set to false`
+ );
+ break;
+ case "qps":
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF),
+ true,
+ `${QUERY_PARAM_STRIP_PREF} has been set to true`
+ );
+ break;
+ case "-qps":
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF),
+ false,
+ `${QUERY_PARAM_STRIP_PREF} has been set to false`
+ );
+ break;
+ case "qpsPBM":
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF),
+ true,
+ `${QUERY_PARAM_STRIP_PBM_PREF} has been set to true`
+ );
+ break;
+ case "-qpsPBM":
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF),
+ false,
+ `${QUERY_PARAM_STRIP_PBM_PREF} has been set to false`
+ );
+ break;
+ case "cookieBehavior0":
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_ACCEPT}`
+ );
+ break;
+ case "cookieBehavior1":
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}`
+ );
+ break;
+ case "cookieBehavior2":
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}`
+ );
+ break;
+ case "cookieBehavior3":
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}`
+ );
+ break;
+ case "cookieBehavior4":
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}`
+ );
+ break;
+ case "cookieBehavior5":
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+ break;
+ case "cookieBehaviorPBM0":
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_ACCEPT}`
+ );
+ break;
+ case "cookieBehaviorPBM1":
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}`
+ );
+ break;
+ case "cookieBehaviorPBM2":
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}`
+ );
+ break;
+ case "cookieBehaviorPBM3":
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}`
+ );
+ break;
+ case "cookieBehaviorPBM4":
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}`
+ );
+ break;
+ case "cookieBehaviorPBM5":
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}`
+ );
+ break;
+ default:
+ ok(false, "unknown option was added to the strict pref");
+ break;
+ }
+ }
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that the content blocking "Custom" category behaves as expected.
+add_task(async function testContentBlockingCustomCategory() {
+ let untouchedPrefs = [
+ TP_PREF,
+ TP_PBM_PREF,
+ NCB_PREF,
+ NCBP_PREF,
+ FP_PREF,
+ STP_PREF,
+ CM_PREF,
+ REFERRER_PREF,
+ REFERRER_TOP_PREF,
+ OCSP_PREF,
+ QUERY_PARAM_STRIP_PREF,
+ QUERY_PARAM_STRIP_PBM_PREF,
+ ];
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+ let strictRadioOption = doc.getElementById("strictRadio");
+ let standardRadioOption = doc.getElementById("standardRadio");
+ let customRadioOption = doc.getElementById("customRadio");
+ let defaults = new Preferences({ defaultBranch: true });
+
+ standardRadioOption.click();
+ await TestUtils.waitForCondition(
+ () => !Services.prefs.prefHasUserValue(TP_PREF)
+ );
+
+ customRadioOption.click();
+ await TestUtils.waitForCondition(
+ () => Services.prefs.getStringPref(CAT_PREF) == "custom"
+ );
+
+ // The custom option will only force change of some prefs, like CAT_PREF. All
+ // other prefs should remain as they were for standard.
+ for (let pref of untouchedPrefs) {
+ ok(
+ !Services.prefs.prefHasUserValue(pref),
+ `the pref ${pref} remains as default value`
+ );
+ }
+
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "custom",
+ `${CAT_PREF} has been set to custom`
+ );
+
+ strictRadioOption.click();
+ await TestUtils.waitForCondition(
+ () => Services.prefs.getStringPref(CAT_PREF) == "strict"
+ );
+
+ // Changing the following prefs should necessarily set CAT_PREF to "custom"
+ for (let pref of [
+ FP_PREF,
+ STP_PREF,
+ CM_PREF,
+ TP_PREF,
+ TP_PBM_PREF,
+ REFERRER_PREF,
+ REFERRER_TOP_PREF,
+ OCSP_PREF,
+ QUERY_PARAM_STRIP_PREF,
+ QUERY_PARAM_STRIP_PBM_PREF,
+ ]) {
+ Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
+ await TestUtils.waitForCondition(
+ () => Services.prefs.getStringPref(CAT_PREF) == "custom"
+ );
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "custom",
+ `${CAT_PREF} has been set to custom`
+ );
+
+ strictRadioOption.click();
+ await TestUtils.waitForCondition(
+ () => Services.prefs.getStringPref(CAT_PREF) == "strict"
+ );
+ }
+
+ // Changing the NCB_PREF should necessarily set CAT_PREF to "custom"
+ let defaultNCB = defaults.get(NCB_PREF);
+ let nonDefaultNCB;
+ switch (defaultNCB) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT;
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+ break;
+ default:
+ ok(
+ false,
+ "Unexpected default value found for " + NCB_PREF + ": " + defaultNCB
+ );
+ break;
+ }
+ Services.prefs.setIntPref(NCB_PREF, nonDefaultNCB);
+ await TestUtils.waitForCondition(() =>
+ Services.prefs.prefHasUserValue(NCB_PREF)
+ );
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "custom",
+ `${CAT_PREF} has been set to custom`
+ );
+
+ strictRadioOption.click();
+ await TestUtils.waitForCondition(
+ () => Services.prefs.getStringPref(CAT_PREF) == "strict"
+ );
+
+ // Changing the NCBP_PREF should necessarily set CAT_PREF to "custom"
+ let defaultNCBP = defaults.get(NCBP_PREF);
+ let nonDefaultNCBP;
+ switch (defaultNCBP) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_REJECT;
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+ break;
+ default:
+ ok(
+ false,
+ "Unexpected default value found for " + NCBP_PREF + ": " + defaultNCBP
+ );
+ break;
+ }
+ Services.prefs.setIntPref(NCBP_PREF, nonDefaultNCBP);
+ await TestUtils.waitForCondition(() =>
+ Services.prefs.prefHasUserValue(NCBP_PREF)
+ );
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "custom",
+ `${CAT_PREF} has been set to custom`
+ );
+
+ for (let pref of untouchedPrefs) {
+ SpecialPowers.clearUserPref(pref);
+ }
+
+ gBrowser.removeCurrentTab();
+});
+
+function checkControlState(doc, controls, enabled) {
+ for (let selector of controls) {
+ for (let control of doc.querySelectorAll(selector)) {
+ if (enabled) {
+ ok(!control.hasAttribute("disabled"), `${selector} is enabled.`);
+ } else {
+ is(
+ control.getAttribute("disabled"),
+ "true",
+ `${selector} is disabled.`
+ );
+ }
+ }
+ }
+}
+
+// Checks that the menulists for tracking protection and cookie blocking are disabled when all TP prefs are off.
+add_task(async function testContentBlockingDependentTPControls() {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ [TP_PREF, false],
+ [TP_PBM_PREF, false],
+ [NCB_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT],
+ [CAT_PREF, "custom"],
+ ],
+ });
+
+ let disabledControls = ["#trackingProtectionMenu", "#blockCookiesMenu"];
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+ checkControlState(doc, disabledControls, false);
+
+ gBrowser.removeCurrentTab();
+});
+
+// Checks that disabling tracking protection also disables email tracking protection.
+add_task(async function testDisableTPCheckBoxDisablesEmailTP() {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ [TP_PREF, false],
+ [TP_PBM_PREF, true],
+ [EMAIL_TP_PREF, false],
+ [EMAIL_TP_PBM_PREF, true],
+ [CAT_PREF, "custom"],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ // Click the checkbox to disable TP and check if this disables Email TP.
+ let tpCheckbox = doc.getElementById(
+ "contentBlockingTrackingProtectionCheckbox"
+ );
+
+ // Verify the initial check state of the tracking protection checkbox.
+ is(
+ tpCheckbox.getAttribute("checked"),
+ "true",
+ "Tracking protection checkbox is checked initially"
+ );
+
+ tpCheckbox.click();
+
+ // Verify the checkbox is unchecked after clicking.
+ is(
+ tpCheckbox.getAttribute("checked"),
+ "",
+ "Tracking protection checkbox is unchecked"
+ );
+
+ // Verify the pref states.
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PREF),
+ false,
+ `${EMAIL_TP_PREF} has been set to false`
+ );
+
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF),
+ false,
+ `${EMAIL_TP_PBM_PREF} has been set to false`
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// Checks that the email tracking prefs set properly with tracking protection
+// drop downs.
+add_task(async function testTPMenuForEmailTP() {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ [TP_PREF, false],
+ [TP_PBM_PREF, true],
+ [EMAIL_TP_PREF, false],
+ [EMAIL_TP_PBM_PREF, true],
+ [CAT_PREF, "custom"],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let menu = doc.querySelector("#trackingProtectionMenu");
+ let always = doc.querySelector(
+ "#trackingProtectionMenu > menupopup > menuitem[value=always]"
+ );
+ let privateElement = doc.querySelector(
+ "#trackingProtectionMenu > menupopup > menuitem[value=private]"
+ );
+
+ // Click the always option on the tracking protection drop down.
+ menu.selectedItem = always;
+ always.click();
+
+ // Verify the pref states.
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PREF),
+ true,
+ `${EMAIL_TP_PREF} has been set to true`
+ );
+
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF),
+ true,
+ `${EMAIL_TP_PBM_PREF} has been set to true`
+ );
+
+ // Click the private-only option on the tracking protection drop down.
+ menu.selectedItem = privateElement;
+ privateElement.click();
+
+ // Verify the pref states.
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PREF),
+ false,
+ `${EMAIL_TP_PREF} has been set to false`
+ );
+
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF),
+ true,
+ `${EMAIL_TP_PBM_PREF} has been set to true`
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// Checks that social media trackers, cryptomining and fingerprinting visibility
+// can be controlled via pref.
+add_task(async function testCustomOptionsVisibility() {
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.cryptomining.preferences.ui.enabled",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.fingerprinting.preferences.ui.enabled",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ false
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.contentDocument;
+ let cryptominersOption = doc.getElementById(
+ "contentBlockingCryptominersOption"
+ );
+ let fingerprintersOption = doc.getElementById(
+ "contentBlockingFingerprintersOption"
+ );
+
+ ok(cryptominersOption.hidden, "Cryptomining is hidden");
+ ok(fingerprintersOption.hidden, "Fingerprinting is hidden");
+
+ gBrowser.removeCurrentTab();
+
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.cryptomining.preferences.ui.enabled",
+ true
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ doc = gBrowser.contentDocument;
+ cryptominersOption = doc.getElementById("contentBlockingCryptominersOption");
+ fingerprintersOption = doc.getElementById(
+ "contentBlockingFingerprintersOption"
+ );
+
+ ok(!cryptominersOption.hidden, "Cryptomining is shown");
+ ok(fingerprintersOption.hidden, "Fingerprinting is hidden");
+
+ gBrowser.removeCurrentTab();
+
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.fingerprinting.preferences.ui.enabled",
+ true
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ doc = gBrowser.contentDocument;
+ cryptominersOption = doc.getElementById("contentBlockingCryptominersOption");
+ fingerprintersOption = doc.getElementById(
+ "contentBlockingFingerprintersOption"
+ );
+
+ ok(!cryptominersOption.hidden, "Cryptomining is shown");
+ ok(!fingerprintersOption.hidden, "Fingerprinting is shown");
+
+ gBrowser.removeCurrentTab();
+
+ // Social media trackers UI should be hidden
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ doc = gBrowser.contentDocument;
+ let socialTrackingUI = [...doc.querySelectorAll(".social-media-option")];
+
+ ok(
+ socialTrackingUI.every(el => el.hidden),
+ "All Social media tracker UI instances are hidden"
+ );
+
+ gBrowser.removeCurrentTab();
+
+ // Social media trackers UI should be visible
+ Services.prefs.setBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ true
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ doc = gBrowser.contentDocument;
+ socialTrackingUI = [...doc.querySelectorAll(".social-media-option")];
+
+ ok(
+ !socialTrackingUI.every(el => el.hidden),
+ "All Social media tracker UI instances are visible"
+ );
+
+ gBrowser.removeCurrentTab();
+
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.cryptomining.preferences.ui.enabled"
+ );
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.fingerprinting.preferences.ui.enabled"
+ );
+ Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled");
+});
+
+// Checks that adding a custom enterprise policy will put the user in the custom category.
+// Other categories will be disabled.
+add_task(async function testPolicyCategorization() {
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "standard",
+ `${CAT_PREF} starts on standard`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(TP_PREF),
+ `${TP_PREF} starts with the default value`
+ );
+ PoliciesPrefTracker.start();
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ EnableTrackingProtection: {
+ Value: true,
+ },
+ },
+ });
+ EnterprisePolicyTesting.checkPolicyPref(TP_PREF, true, false);
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "custom",
+ `${CAT_PREF} has been set to custom`
+ );
+
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "standard",
+ `${CAT_PREF} starts on standard`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCB_PREF),
+ `${NCB_PREF} starts with the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCBP_PREF),
+ `${NCBP_PREF} starts with the default value`
+ );
+
+ let uiUpdatedPromise = TestUtils.topicObserved("privacy-pane-tp-ui-updated");
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Cookies: {
+ AcceptThirdParty: "never",
+ Locked: true,
+ },
+ },
+ });
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await uiUpdatedPromise;
+
+ EnterprisePolicyTesting.checkPolicyPref(
+ NCB_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ true
+ );
+ EnterprisePolicyTesting.checkPolicyPref(
+ NCBP_PREF,
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ true
+ );
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "custom",
+ `${CAT_PREF} has been set to custom`
+ );
+
+ let doc = gBrowser.contentDocument;
+ let strictRadioOption = doc.getElementById("strictRadio");
+ let standardRadioOption = doc.getElementById("standardRadio");
+ is(strictRadioOption.disabled, true, "the strict option is disabled");
+ is(standardRadioOption.disabled, true, "the standard option is disabled");
+
+ gBrowser.removeCurrentTab();
+
+ // Cleanup after this particular test.
+ if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) {
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Cookies: {
+ Locked: false,
+ },
+ },
+ });
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ }
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.INACTIVE,
+ "Engine is inactive at the end of the test"
+ );
+
+ EnterprisePolicyTesting.resetRunOnceState();
+ PoliciesPrefTracker.stop();
+});
+
+// Tests that changing a content blocking pref shows the content blocking warning
+// to reload tabs to apply changes.
+add_task(async function testContentBlockingReloadWarning() {
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+ let reloadWarnings = [
+ ...doc.querySelectorAll(".content-blocking-warning.reload-tabs"),
+ ];
+ let allHidden = reloadWarnings.every(el => el.hidden);
+ ok(allHidden, "all of the warnings to reload tabs are initially hidden");
+
+ Services.prefs.setStringPref(CAT_PREF, "strict");
+
+ let strictWarning = doc.querySelector(
+ "#contentBlockingOptionStrict .content-blocking-warning.reload-tabs"
+ );
+ ok(
+ !BrowserTestUtils.is_hidden(strictWarning),
+ "The warning in the strict section should be showing"
+ );
+
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that changing a content blocking pref does not show the content blocking warning
+// if it is the only tab.
+add_task(async function testContentBlockingReloadWarningSingleTab() {
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, PRIVACY_PAGE);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ PRIVACY_PAGE
+ );
+
+ let reloadWarnings = [
+ ...gBrowser.contentDocument.querySelectorAll(
+ ".content-blocking-warning.reload-tabs"
+ ),
+ ];
+ ok(reloadWarnings.length, "must have at least one reload warning");
+ ok(
+ reloadWarnings.every(el => el.hidden),
+ "all of the warnings to reload tabs are initially hidden"
+ );
+
+ is(BrowserWindowTracker.windowCount, 1, "There is only one window open");
+ is(gBrowser.tabs.length, 1, "There is only one tab open");
+ Services.prefs.setStringPref(CAT_PREF, "strict");
+
+ ok(
+ reloadWarnings.every(el => el.hidden),
+ "all of the warnings to reload tabs are still hidden"
+ );
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:newtab");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+});
+
+// Checks that the reload tabs message reloads all tabs except the active tab.
+add_task(async function testReloadTabsMessage() {
+ Services.prefs.setStringPref(CAT_PREF, "strict");
+ let exampleTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ let examplePinnedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ gBrowser.pinTab(examplePinnedTab);
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+ let standardWarning = doc.querySelector(
+ "#contentBlockingOptionStandard .content-blocking-warning.reload-tabs"
+ );
+ let standardReloadButton = doc.querySelector(
+ "#contentBlockingOptionStandard .reload-tabs-button"
+ );
+
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ ok(
+ !BrowserTestUtils.is_hidden(standardWarning),
+ "The warning in the standard section should be showing"
+ );
+
+ let exampleTabBrowserDiscardedPromise = BrowserTestUtils.waitForEvent(
+ exampleTab,
+ "TabBrowserDiscarded"
+ );
+ let examplePinnedTabLoadPromise = BrowserTestUtils.browserLoaded(
+ examplePinnedTab.linkedBrowser
+ );
+ standardReloadButton.click();
+ // The pinned example page had a load event
+ await examplePinnedTabLoadPromise;
+ // The other one had its browser discarded
+ await exampleTabBrowserDiscardedPromise;
+
+ ok(
+ BrowserTestUtils.is_hidden(standardWarning),
+ "The warning in the standard section should have hidden after being clicked"
+ );
+
+ // cleanup
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ gBrowser.removeTab(exampleTab);
+ gBrowser.removeTab(examplePinnedTab);
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_contentblocking_categories.js b/browser/components/preferences/tests/browser_contentblocking_categories.js
new file mode 100644
index 0000000000..3b9e16a2fe
--- /dev/null
+++ b/browser/components/preferences/tests/browser_contentblocking_categories.js
@@ -0,0 +1,487 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env webextensions */
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled";
+const NCB_PREF = "network.cookie.cookieBehavior";
+const NCBP_PREF = "network.cookie.cookieBehavior.pbmode";
+const CAT_PREF = "browser.contentblocking.category";
+const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled";
+const CM_PREF = "privacy.trackingprotection.cryptomining.enabled";
+const STP_PREF = "privacy.trackingprotection.socialtracking.enabled";
+const EMAIL_TP_PREF = "privacy.trackingprotection.emailtracking.enabled";
+const EMAIL_TP_PBM_PREF =
+ "privacy.trackingprotection.emailtracking.pbmode.enabled";
+const LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled";
+const REFERRER_PREF = "network.http.referer.disallowCrossSiteRelaxingDefault";
+const REFERRER_TOP_PREF =
+ "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation";
+const OCSP_PREF = "privacy.partition.network_state.ocsp_cache";
+const QUERY_PARAM_STRIP_PREF = "privacy.query_stripping.enabled";
+const QUERY_PARAM_STRIP_PBM_PREF = "privacy.query_stripping.enabled.pbmode";
+const STRICT_DEF_PREF = "browser.contentblocking.features.strict";
+
+// Tests that the content blocking standard category definition is based on the default settings of
+// the content blocking prefs.
+// Changing the definition does not remove the user from the category.
+add_task(async function testContentBlockingStandardDefinition() {
+ Services.prefs.setStringPref(CAT_PREF, "strict");
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "standard",
+ `${CAT_PREF} starts on standard`
+ );
+
+ ok(
+ !Services.prefs.prefHasUserValue(TP_PREF),
+ `${TP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(TP_PBM_PREF),
+ `${TP_PBM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(FP_PREF),
+ `${FP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(CM_PREF),
+ `${CM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(STP_PREF),
+ `${STP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(EMAIL_TP_PREF),
+ `${EMAIL_TP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF),
+ `${EMAIL_TP_PBM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCB_PREF),
+ `${NCB_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCBP_PREF),
+ `${NCBP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(LEVEL2_PREF),
+ `${LEVEL2_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(REFERRER_PREF),
+ `${REFERRER_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF),
+ `${REFERRER_TOP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(OCSP_PREF),
+ `${OCSP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF),
+ `${QUERY_PARAM_STRIP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF),
+ `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value`
+ );
+
+ let defaults = Services.prefs.getDefaultBranch("");
+ let originalTP = defaults.getBoolPref(TP_PREF);
+ let originalTPPBM = defaults.getBoolPref(TP_PBM_PREF);
+ let originalFP = defaults.getBoolPref(FP_PREF);
+ let originalCM = defaults.getBoolPref(CM_PREF);
+ let originalSTP = defaults.getBoolPref(STP_PREF);
+ let originalEmailTP = defaults.getBoolPref(EMAIL_TP_PREF);
+ let originalEmailTPPBM = defaults.getBoolPref(EMAIL_TP_PBM_PREF);
+ let originalNCB = defaults.getIntPref(NCB_PREF);
+ let originalNCBP = defaults.getIntPref(NCBP_PREF);
+ let originalLEVEL2 = defaults.getBoolPref(LEVEL2_PREF);
+ let originalREFERRER = defaults.getBoolPref(REFERRER_PREF);
+ let originalREFERRERTOP = defaults.getBoolPref(REFERRER_TOP_PREF);
+ let originalOCSP = defaults.getBoolPref(OCSP_PREF);
+ let originalQueryParamStrip = defaults.getBoolPref(QUERY_PARAM_STRIP_PREF);
+ let originalQueryParamStripPBM = defaults.getBoolPref(
+ QUERY_PARAM_STRIP_PBM_PREF
+ );
+
+ let nonDefaultNCB;
+ switch (originalNCB) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT;
+ break;
+ default:
+ nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+ break;
+ }
+ let nonDefaultNCBP;
+ switch (originalNCBP) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_REJECT;
+ break;
+ default:
+ nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+ break;
+ }
+ defaults.setIntPref(NCB_PREF, nonDefaultNCB);
+ defaults.setIntPref(NCBP_PREF, nonDefaultNCBP);
+ defaults.setBoolPref(TP_PREF, !originalTP);
+ defaults.setBoolPref(TP_PBM_PREF, !originalTPPBM);
+ defaults.setBoolPref(FP_PREF, !originalFP);
+ defaults.setBoolPref(CM_PREF, !originalCM);
+ defaults.setBoolPref(CM_PREF, !originalSTP);
+ defaults.setBoolPref(EMAIL_TP_PREF, !originalEmailTP);
+ defaults.setBoolPref(EMAIL_TP_PBM_PREF, !originalEmailTPPBM);
+ defaults.setIntPref(NCB_PREF, !originalNCB);
+ defaults.setBoolPref(LEVEL2_PREF, !originalLEVEL2);
+ defaults.setBoolPref(REFERRER_PREF, !originalREFERRER);
+ defaults.setBoolPref(REFERRER_TOP_PREF, !originalREFERRERTOP);
+ defaults.setBoolPref(OCSP_PREF, !originalOCSP);
+ defaults.setBoolPref(QUERY_PARAM_STRIP_PREF, !originalQueryParamStrip);
+ defaults.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, !originalQueryParamStripPBM);
+
+ ok(
+ !Services.prefs.prefHasUserValue(TP_PREF),
+ `${TP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(TP_PBM_PREF),
+ `${TP_PBM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(FP_PREF),
+ `${FP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(CM_PREF),
+ `${CM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(STP_PREF),
+ `${STP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(EMAIL_TP_PREF),
+ `${EMAIL_TP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF),
+ `${EMAIL_TP_PBM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCB_PREF),
+ `${NCB_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCBP_PREF),
+ `${NCBP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(LEVEL2_PREF),
+ `${LEVEL2_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(REFERRER_PREF),
+ `${REFERRER_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF),
+ `${REFERRER_TOP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(OCSP_PREF),
+ `${OCSP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF),
+ `${QUERY_PARAM_STRIP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF),
+ `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value`
+ );
+
+ // cleanup
+ defaults.setIntPref(NCB_PREF, originalNCB);
+ defaults.setBoolPref(TP_PREF, originalTP);
+ defaults.setBoolPref(TP_PBM_PREF, originalTPPBM);
+ defaults.setBoolPref(FP_PREF, originalFP);
+ defaults.setBoolPref(CM_PREF, originalCM);
+ defaults.setBoolPref(STP_PREF, originalSTP);
+ defaults.setBoolPref(EMAIL_TP_PREF, originalEmailTP);
+ defaults.setBoolPref(EMAIL_TP_PBM_PREF, originalEmailTPPBM);
+ defaults.setIntPref(NCB_PREF, originalNCB);
+ defaults.setIntPref(NCBP_PREF, originalNCBP);
+ defaults.setBoolPref(LEVEL2_PREF, originalLEVEL2);
+ defaults.setBoolPref(REFERRER_PREF, originalREFERRER);
+ defaults.setBoolPref(REFERRER_TOP_PREF, originalREFERRERTOP);
+ defaults.setBoolPref(OCSP_PREF, originalOCSP);
+ defaults.setBoolPref(QUERY_PARAM_STRIP_PREF, originalQueryParamStrip);
+ defaults.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, originalQueryParamStripPBM);
+});
+
+// Tests that the content blocking strict category definition changes the behavior
+// of the strict category pref and all prefs it controls.
+// Changing the definition does not remove the user from the category.
+add_task(async function testContentBlockingStrictDefinition() {
+ let defaults = Services.prefs.getDefaultBranch("");
+ let originalStrictPref = defaults.getStringPref(STRICT_DEF_PREF);
+ defaults.setStringPref(
+ STRICT_DEF_PREF,
+ "tp,tpPrivate,fp,cm,cookieBehavior0,cookieBehaviorPBM0,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp,qps,qpsPBM"
+ );
+ Services.prefs.setStringPref(CAT_PREF, "strict");
+ is(
+ Services.prefs.getStringPref(CAT_PREF),
+ "strict",
+ `${CAT_PREF} has changed to strict`
+ );
+
+ ok(
+ !Services.prefs.prefHasUserValue(STRICT_DEF_PREF),
+ `We changed the default value of ${STRICT_DEF_PREF}`
+ );
+ is(
+ Services.prefs.getStringPref(STRICT_DEF_PREF),
+ "tp,tpPrivate,fp,cm,cookieBehavior0,cookieBehaviorPBM0,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp,qps,qpsPBM",
+ `${STRICT_DEF_PREF} changed to what we set.`
+ );
+
+ is(
+ Services.prefs.getBoolPref(TP_PREF),
+ true,
+ `${TP_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(TP_PBM_PREF),
+ true,
+ `${TP_PBM_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(FP_PREF),
+ true,
+ `${CM_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(CM_PREF),
+ true,
+ `${CM_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(STP_PREF),
+ true,
+ `${STP_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PREF),
+ true,
+ `${EMAIL_TP_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF),
+ true,
+ `${EMAIL_TP_PBM_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ `${NCB_PREF} has been set to BEHAVIOR_ACCEPT`
+ );
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ `${NCBP_PREF} has been set to BEHAVIOR_ACCEPT`
+ );
+ is(
+ Services.prefs.getBoolPref(LEVEL2_PREF),
+ true,
+ `${LEVEL2_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(REFERRER_PREF),
+ true,
+ `${REFERRER_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(REFERRER_TOP_PREF),
+ true,
+ `${REFERRER_TOP_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(OCSP_PREF),
+ true,
+ `${OCSP_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF),
+ true,
+ `${QUERY_PARAM_STRIP_PREF} pref has been set to true`
+ );
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF),
+ true,
+ `${QUERY_PARAM_STRIP_PBM_PREF} pref has been set to true`
+ );
+
+ // Note, if a pref is not listed it will use the default value, however this is only meant as a
+ // backup if a mistake is made. The UI will not respond correctly.
+ defaults.setStringPref(STRICT_DEF_PREF, "");
+ ok(
+ !Services.prefs.prefHasUserValue(TP_PREF),
+ `${TP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(TP_PBM_PREF),
+ `${TP_PBM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(FP_PREF),
+ `${FP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(CM_PREF),
+ `${CM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(STP_PREF),
+ `${STP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(EMAIL_TP_PREF),
+ `${EMAIL_TP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF),
+ `${EMAIL_TP_PBM_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCB_PREF),
+ `${NCB_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NCBP_PREF),
+ `${NCBP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(LEVEL2_PREF),
+ `${LEVEL2_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(REFERRER_PREF),
+ `${REFERRER_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF),
+ `${REFERRER_TOP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(OCSP_PREF),
+ `${OCSP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF),
+ `${QUERY_PARAM_STRIP_PREF} pref has the default value`
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF),
+ `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value`
+ );
+
+ defaults.setStringPref(
+ STRICT_DEF_PREF,
+ "-tpPrivate,-fp,-cm,-tp,cookieBehavior3,cookieBehaviorPBM2,-stp,-emailTP,-emailTPPrivate,-lvl2,-rp,-ocsp,-qps,-qpsPBM"
+ );
+ is(
+ Services.prefs.getBoolPref(TP_PREF),
+ false,
+ `${TP_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(TP_PBM_PREF),
+ false,
+ `${TP_PBM_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(FP_PREF),
+ false,
+ `${FP_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(CM_PREF),
+ false,
+ `${CM_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(STP_PREF),
+ false,
+ `${STP_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PREF),
+ false,
+ `${EMAIL_TP_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF),
+ false,
+ `${EMAIL_TP_PBM_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getIntPref(NCB_PREF),
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN,
+ `${NCB_PREF} has been set to BEHAVIOR_REJECT_TRACKER`
+ );
+ is(
+ Services.prefs.getIntPref(NCBP_PREF),
+ Ci.nsICookieService.BEHAVIOR_REJECT,
+ `${NCBP_PREF} has been set to BEHAVIOR_REJECT`
+ );
+ is(
+ Services.prefs.getBoolPref(LEVEL2_PREF),
+ false,
+ `${LEVEL2_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(REFERRER_PREF),
+ false,
+ `${REFERRER_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(REFERRER_TOP_PREF),
+ false,
+ `${REFERRER_TOP_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(OCSP_PREF),
+ false,
+ `${OCSP_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF),
+ false,
+ `${QUERY_PARAM_STRIP_PREF} pref has been set to false`
+ );
+ is(
+ Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF),
+ false,
+ `${QUERY_PARAM_STRIP_PBM_PREF} pref has been set to false`
+ );
+
+ // cleanup
+ defaults.setStringPref(STRICT_DEF_PREF, originalStrictPref);
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+});
diff --git a/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js b/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js
new file mode 100644
index 0000000000..249a42317a
--- /dev/null
+++ b/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the TCP info box in the ETP standard section of about:preferences#privacy.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior";
+const CAT_PREF = "browser.contentblocking.category";
+
+const LEARN_MORE_URL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "total-cookie-protection";
+
+const {
+ BEHAVIOR_REJECT_TRACKER,
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+} = Ci.nsICookieService;
+
+async function testTCPSection({ dFPIEnabled }) {
+ info(
+ "Testing TCP preferences section in standard " +
+ JSON.stringify({ dFPIEnabled })
+ );
+
+ // In order to test the "standard" category we need to set the default value
+ // for the cookie behavior pref. A user value would get cleared as soon as we
+ // switch to "standard".
+ Services.prefs
+ .getDefaultBranch("")
+ .setIntPref(
+ COOKIE_BEHAVIOR_PREF,
+ dFPIEnabled
+ ? BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ : BEHAVIOR_REJECT_TRACKER
+ );
+
+ // Setting to standard category explicitly, since changing the default cookie
+ // behavior still switches us to custom initially.
+ await SpecialPowers.pushPrefEnv({
+ set: [[CAT_PREF, "standard"]],
+ });
+
+ const uiEnabled =
+ Services.prefs.getIntPref(COOKIE_BEHAVIOR_PREF) ==
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let standardRadioOption = doc.getElementById("standardRadio");
+ let strictRadioOption = doc.getElementById("strictRadio");
+ let customRadioOption = doc.getElementById("customRadio");
+
+ ok(standardRadioOption.selected, "Standard category is selected");
+
+ let etpStandardTCPBox = doc.getElementById("etpStandardTCPBox");
+ is(
+ BrowserTestUtils.is_visible(etpStandardTCPBox),
+ uiEnabled,
+ `TCP section in standard is ${uiEnabled ? " " : "not "}visible.`
+ );
+
+ if (uiEnabled) {
+ // If visible, test the TCP section elements.
+ let learnMoreLink = etpStandardTCPBox.querySelector("#tcp-learn-more-link");
+ ok(learnMoreLink, "Should have a learn more link");
+ BrowserTestUtils.is_visible(
+ learnMoreLink,
+ "Learn more link should be visible."
+ );
+ ok(
+ learnMoreLink.href && !learnMoreLink.href.startsWith("about:blank"),
+ "Learn more link should be valid."
+ );
+ is(
+ learnMoreLink.href,
+ LEARN_MORE_URL,
+ "Learn more link should have the correct target."
+ );
+
+ let description = etpStandardTCPBox.querySelector(".tail-with-learn-more");
+ ok(description, "Should have a description element.");
+ BrowserTestUtils.is_visible(description, "Description should be visible.");
+
+ let title = etpStandardTCPBox.querySelector(
+ ".content-blocking-warning-title"
+ );
+ ok(title, "Should have a title element.");
+ BrowserTestUtils.is_visible(title, "Title should be visible.");
+ }
+
+ info("Switch to ETP strict.");
+ let categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "strict");
+ strictRadioOption.click();
+ await categoryPrefChange;
+ ok(
+ !BrowserTestUtils.is_visible(etpStandardTCPBox),
+ "When strict is selected TCP UI is not visible."
+ );
+
+ info("Switch to ETP custom.");
+ categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "custom");
+ customRadioOption.click();
+ await categoryPrefChange;
+ ok(
+ !BrowserTestUtils.is_visible(etpStandardTCPBox),
+ "When custom is selected TCP UI is not visible."
+ );
+
+ info("Switch back to standard and ensure we show the TCP UI again.");
+ categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "standard");
+ standardRadioOption.click();
+ await categoryPrefChange;
+ is(
+ BrowserTestUtils.is_visible(etpStandardTCPBox),
+ uiEnabled,
+ `TCP section in standard is ${uiEnabled ? " " : "not "}visible.`
+ );
+
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+ Services.prefs.setStringPref(CAT_PREF, "standard");
+}
+
+add_setup(async function () {
+ // Register cleanup function to restore default cookie behavior.
+ const defaultPrefs = Services.prefs.getDefaultBranch("");
+ const previousDefaultCB = defaultPrefs.getIntPref(COOKIE_BEHAVIOR_PREF);
+
+ registerCleanupFunction(function () {
+ defaultPrefs.setIntPref(COOKIE_BEHAVIOR_PREF, previousDefaultCB);
+ });
+});
+
+// Clients which don't have dFPI enabled should not see the
+// preferences section.
+add_task(async function test_dfpi_disabled() {
+ await testTCPSection({ dFPIEnabled: false });
+});
+
+add_task(async function test_dfpi_enabled() {
+ await testTCPSection({ dFPIEnabled: true });
+});
diff --git a/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js
new file mode 100644
index 0000000000..1b70170ddc
--- /dev/null
+++ b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js
@@ -0,0 +1,299 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+if (AppConstants.TSAN || AppConstants.DEBUG) {
+ requestLongerTimeout(2);
+}
+
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml";
+
+async function openCookiesDialog(doc) {
+ let cookieExceptionsButton = doc.getElementById("cookieExceptions");
+ ok(cookieExceptionsButton, "cookieExceptionsButton found");
+ let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL);
+ cookieExceptionsButton.click();
+ let dialog = await dialogPromise;
+ return dialog;
+}
+
+function checkCookiesDialog(dialog) {
+ ok(dialog, "dialog loaded");
+ let buttonIds = ["removePermission", "removeAllPermissions"];
+
+ for (let buttonId of buttonIds) {
+ let button = dialog.document.getElementById(buttonId);
+ ok(button, `${buttonId} found`);
+ }
+
+ let dialogEl = dialog.document.querySelector("dialog");
+ let acceptBtn = dialogEl.getButton("accept");
+ let cancelBtn = dialogEl.getButton("cancel");
+
+ ok(!acceptBtn.hidden, "acceptButton found");
+ ok(!cancelBtn.hidden, "cancelButton found");
+}
+
+function addNewPermission(websiteAddress, dialog) {
+ let url = dialog.document.getElementById("url");
+ let buttonDialog = dialog.document.getElementById("btnBlock");
+ let permissionsBox = dialog.document.getElementById("permissionsBox");
+ let currentPermissions = permissionsBox.itemCount;
+
+ url.value = websiteAddress;
+ url.dispatchEvent(new Event("input", { bubbles: true }));
+ is(
+ buttonDialog.hasAttribute("disabled"),
+ false,
+ "When the user add an url the button should be clickable"
+ );
+ buttonDialog.click();
+
+ is(
+ permissionsBox.itemCount,
+ currentPermissions + 1,
+ "Website added in url should be in the list"
+ );
+}
+
+async function cleanList(dialog) {
+ let removeAllButton = dialog.document.getElementById("removeAllPermissions");
+ if (!removeAllButton.hasAttribute("disabled")) {
+ removeAllButton.click();
+ }
+}
+
+function addData(websites, dialog) {
+ for (let website of websites) {
+ addNewPermission(website, dialog);
+ }
+}
+
+function deletePermission(permission, dialog) {
+ let permissionsBox = dialog.document.getElementById("permissionsBox");
+ let elements = permissionsBox.getElementsByAttribute("origin", permission);
+ is(elements.length, 1, "It should find only one entry");
+ permissionsBox.selectItem(elements[0]);
+ let removePermissionButton =
+ dialog.document.getElementById("removePermission");
+ is(
+ removePermissionButton.hasAttribute("disabled"),
+ false,
+ "The button should be clickable to remove selected item"
+ );
+ removePermissionButton.click();
+}
+
+function save(dialog) {
+ let saveButton = dialog.document.querySelector("dialog").getButton("accept");
+ saveButton.click();
+}
+
+function cancel(dialog) {
+ let cancelButton = dialog.document
+ .querySelector("dialog")
+ .getButton("cancel");
+ ok(!cancelButton.hidden, "cancelButton found");
+ cancelButton.click();
+}
+
+async function checkExpected(expected, doc) {
+ let dialog = await openCookiesDialog(doc);
+ let permissionsBox = dialog.document.getElementById("permissionsBox");
+
+ is(
+ permissionsBox.itemCount,
+ expected.length,
+ `There should be ${expected.length} elements in the list`
+ );
+
+ for (let website of expected) {
+ let elements = permissionsBox.getElementsByAttribute("origin", website);
+ is(elements.length, 1, "It should find only one entry");
+ }
+ return dialog;
+}
+
+async function runTest(test, websites, doc) {
+ let dialog = await openCookiesDialog(doc);
+ checkCookiesDialog(dialog);
+
+ if (test.needPreviousData) {
+ addData(websites, dialog);
+ save(dialog);
+ dialog = await openCookiesDialog(doc);
+ }
+
+ for (let step of test.steps) {
+ switch (step) {
+ case "addNewPermission":
+ addNewPermission(test.newData, dialog);
+ break;
+ case "deletePermission":
+ deletePermission(test.newData, dialog);
+ break;
+ case "deleteAllPermission":
+ await cleanList(dialog);
+ break;
+ case "save":
+ save(dialog);
+ break;
+ case "cancel":
+ cancel(dialog);
+ break;
+ case "openPane":
+ dialog = await openCookiesDialog(doc);
+ break;
+ default:
+ // code block
+ }
+ }
+ dialog = await checkExpected(test.expected, doc);
+ await cleanList(dialog);
+ save(dialog);
+}
+
+add_task(async function checkPermissions() {
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = win.document;
+ let websites = ["http://test1.com", "http://test2.com"];
+
+ let tests = [
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "save"],
+ expected: ["https://mytest.com"], // when open the pane again it should find this in the list
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "cancel"],
+ expected: [],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "deletePermission", "save"],
+ expected: [],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "deletePermission", "cancel"],
+ expected: [],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: [
+ "addNewPermission",
+ "save",
+ "openPane",
+ "deletePermission",
+ "save",
+ ],
+ expected: [],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: [
+ "addNewPermission",
+ "save",
+ "openPane",
+ "deletePermission",
+ "cancel",
+ ],
+ expected: ["https://mytest.com"],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "deleteAllPermission", "save"],
+ expected: [],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "deleteAllPermission", "cancel"],
+ expected: [],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: [
+ "addNewPermission",
+ "save",
+ "openPane",
+ "deleteAllPermission",
+ "save",
+ ],
+ expected: [],
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: [
+ "addNewPermission",
+ "save",
+ "openPane",
+ "deleteAllPermission",
+ "cancel",
+ ],
+ expected: ["https://mytest.com"],
+ },
+ {
+ needPreviousData: true,
+ newData: "https://mytest.com",
+ steps: ["deleteAllPermission", "save"],
+ expected: [],
+ },
+ {
+ needPreviousData: true,
+ newData: "https://mytest.com",
+ steps: ["deleteAllPermission", "cancel"],
+ expected: websites,
+ },
+ {
+ needPreviousData: true,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "save"],
+ expected: (function () {
+ let result = websites.slice();
+ result.push("https://mytest.com");
+ return result;
+ })(),
+ },
+ {
+ needPreviousData: true,
+ newData: "https://mytest.com",
+ steps: ["addNewPermission", "cancel"],
+ expected: websites,
+ },
+ {
+ needPreviousData: false,
+ newData: "https://mytest.com",
+ steps: [
+ "addNewPermission",
+ "save",
+ "openPane",
+ "deleteAllPermission",
+ "addNewPermission",
+ "save",
+ ],
+ expected: ["https://mytest.com"],
+ },
+ ];
+
+ for (let test of tests) {
+ await runTest(test, websites, doc);
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_cookies_exceptions.js b/browser/components/preferences/tests/browser_cookies_exceptions.js
new file mode 100644
index 0000000000..d2d538a48a
--- /dev/null
+++ b/browser/components/preferences/tests/browser_cookies_exceptions.js
@@ -0,0 +1,568 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(3);
+
+add_task(async function testAllow() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ assertListContents(params, []);
+
+ params.url.value = "test.com";
+ params.btnAllow.doCommand();
+
+ assertListContents(params, [
+ ["http://test.com", params.allowL10nId],
+ ["https://test.com", params.allowL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "http://test.com",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "https://test.com",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testBlock() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "test.com";
+ params.btnBlock.doCommand();
+
+ assertListContents(params, [
+ ["http://test.com", params.denyL10nId],
+ ["https://test.com", params.denyL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "http://test.com",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.DENY_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "https://test.com",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.DENY_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testAllowAgain() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "test.com";
+ params.btnAllow.doCommand();
+
+ assertListContents(params, [
+ ["http://test.com", params.allowL10nId],
+ ["https://test.com", params.allowL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "http://test.com",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "https://test.com",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testRemove() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ while (params.richlistbox.itemCount) {
+ params.richlistbox.selectedIndex = 0;
+ params.btnRemove.doCommand();
+ }
+ assertListContents(params, []);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ let richlistItems = params.richlistbox.getElementsByAttribute(
+ "origin",
+ "*"
+ );
+ let observances = [];
+ for (let item of richlistItems) {
+ observances.push({
+ type: "cookie",
+ origin: item.getAttribute("origin"),
+ data: "deleted",
+ });
+ }
+ return observances;
+ }
+ );
+});
+
+add_task(async function testAdd() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ let uri = Services.io.newURI("http://test.com");
+ PermissionTestUtils.add(
+ uri,
+ "popup",
+ Ci.nsIPermissionManager.DENY_ACTION
+ );
+
+ info("Adding unrelated permission should not change display.");
+ assertListContents(params, []);
+
+ apply();
+ await observeAllPromise;
+
+ PermissionTestUtils.remove(uri, "popup");
+ },
+ params => {
+ return [
+ {
+ type: "popup",
+ origin: "http://test.com",
+ data: "added",
+ capability: Ci.nsIPermissionManager.DENY_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testAllowHTTPSWithPort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "https://test.com:12345";
+ params.btnAllow.doCommand();
+
+ assertListContents(params, [
+ ["https://test.com:12345", params.allowL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "https://test.com:12345",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testBlockHTTPSWithPort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "https://test.com:12345";
+ params.btnBlock.doCommand();
+
+ assertListContents(params, [
+ ["https://test.com:12345", params.denyL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "https://test.com:12345",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.DENY_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testAllowAgainHTTPSWithPort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "https://test.com:12345";
+ params.btnAllow.doCommand();
+
+ assertListContents(params, [
+ ["https://test.com:12345", params.allowL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "https://test.com:12345",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testRemoveHTTPSWithPort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ while (params.richlistbox.itemCount) {
+ params.richlistbox.selectedIndex = 0;
+ params.btnRemove.doCommand();
+ }
+
+ assertListContents(params, []);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ let richlistItems = params.richlistbox.getElementsByAttribute(
+ "origin",
+ "*"
+ );
+ let observances = [];
+ for (let item of richlistItems) {
+ observances.push({
+ type: "cookie",
+ origin: item.getAttribute("origin"),
+ data: "deleted",
+ });
+ }
+ return observances;
+ }
+ );
+});
+
+add_task(async function testAllowPort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "localhost:12345";
+ params.btnAllow.doCommand();
+
+ assertListContents(params, [
+ ["http://localhost:12345", params.allowL10nId],
+ ["https://localhost:12345", params.allowL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "http://localhost:12345",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "https://localhost:12345",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testBlockPort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "localhost:12345";
+ params.btnBlock.doCommand();
+
+ assertListContents(params, [
+ ["http://localhost:12345", params.denyL10nId],
+ ["https://localhost:12345", params.denyL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "http://localhost:12345",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.DENY_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "https://localhost:12345",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.DENY_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testAllowAgainPort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ params.url.value = "localhost:12345";
+ params.btnAllow.doCommand();
+
+ assertListContents(params, [
+ ["http://localhost:12345", params.allowL10nId],
+ ["https://localhost:12345", params.allowL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "http://localhost:12345",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "https://localhost:12345",
+ data: "changed",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testRemovePort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ while (params.richlistbox.itemCount) {
+ params.richlistbox.selectedIndex = 0;
+ params.btnRemove.doCommand();
+ }
+
+ assertListContents(params, []);
+
+ apply();
+ await observeAllPromise;
+ },
+ params => {
+ let richlistItems = params.richlistbox.getElementsByAttribute(
+ "origin",
+ "*"
+ );
+ let observances = [];
+ for (let item of richlistItems) {
+ observances.push({
+ type: "cookie",
+ origin: item.getAttribute("origin"),
+ data: "deleted",
+ });
+ }
+ return observances;
+ }
+ );
+});
+
+add_task(async function testSort() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ // Sort by site name.
+ EventUtils.synthesizeMouseAtCenter(
+ params.doc.getElementById("siteCol"),
+ {},
+ params.doc.defaultView
+ );
+
+ for (let URL of ["http://a", "http://z", "http://b"]) {
+ let URI = Services.io.newURI(URL);
+ PermissionTestUtils.add(
+ URI,
+ "cookie",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+ }
+
+ assertListContents(params, [
+ ["http://a", params.allowL10nId],
+ ["http://b", params.allowL10nId],
+ ["http://z", params.allowL10nId],
+ ]);
+
+ // Sort by site name in descending order.
+ EventUtils.synthesizeMouseAtCenter(
+ params.doc.getElementById("siteCol"),
+ {},
+ params.doc.defaultView
+ );
+
+ assertListContents(params, [
+ ["http://z", params.allowL10nId],
+ ["http://b", params.allowL10nId],
+ ["http://a", params.allowL10nId],
+ ]);
+
+ apply();
+ await observeAllPromise;
+
+ for (let URL of ["http://a", "http://z", "http://b"]) {
+ let uri = Services.io.newURI(URL);
+ PermissionTestUtils.remove(uri, "cookie");
+ }
+ },
+ params => {
+ return [
+ {
+ type: "cookie",
+ origin: "http://a",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "http://z",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ {
+ type: "cookie",
+ origin: "http://b",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ },
+ ];
+ }
+ );
+});
+
+add_task(async function testPrivateBrowsingSessionPermissionsAreHidden() {
+ await runTest(
+ async (params, observeAllPromise, apply) => {
+ assertListContents(params, []);
+
+ let uri = Services.io.newURI("http://test.com");
+ let privateBrowsingPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(uri, {
+ privateBrowsingId: 1,
+ });
+
+ // Add a session permission for private browsing.
+ PermissionTestUtils.add(
+ privateBrowsingPrincipal,
+ "cookie",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_SESSION
+ );
+
+ assertListContents(params, []);
+
+ PermissionTestUtils.remove(uri, "cookie");
+ },
+ params => {
+ return [];
+ }
+ );
+});
+
+function assertListContents(params, expected) {
+ Assert.equal(params.richlistbox.itemCount, expected.length);
+
+ for (let i = 0; i < expected.length; i++) {
+ let website = expected[i][0];
+ let elements = params.richlistbox.getElementsByAttribute("origin", website);
+ Assert.equal(elements.length, 1); // "It should find only one coincidence"
+ Assert.equal(
+ elements[0]
+ .querySelector(".website-capability-value")
+ .getAttribute("data-l10n-id"),
+ expected[i][1]
+ );
+ }
+}
+
+async function runTest(test, getObservances) {
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("privacy.history.custom");
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let historyMode = doc.getElementById("historyMode");
+ historyMode.value = "custom";
+ historyMode.doCommand();
+
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml"
+ );
+ doc.getElementById("cookieExceptions").doCommand();
+
+ let win = await promiseSubDialogLoaded;
+
+ doc = win.document;
+ let params = {
+ doc,
+ richlistbox: doc.getElementById("permissionsBox"),
+ url: doc.getElementById("url"),
+ btnAllow: doc.getElementById("btnAllow"),
+ btnBlock: doc.getElementById("btnBlock"),
+ btnRemove: doc.getElementById("removePermission"),
+ allowL10nId: win.gPermissionManager._getCapabilityL10nId(
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ ),
+ denyL10nId: win.gPermissionManager._getCapabilityL10nId(
+ Ci.nsIPermissionManager.DENY_ACTION
+ ),
+ allow: Ci.nsIPermissionManager.ALLOW_ACTION,
+ deny: Ci.nsIPermissionManager.DENY_ACTION,
+ };
+ let btnApplyChanges = doc.querySelector("dialog").getButton("accept");
+ let observances = getObservances(params);
+ let observeAllPromise = createObserveAllPromise(observances);
+
+ await test(params, observeAllPromise, () => btnApplyChanges.doCommand());
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
diff --git a/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js b/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js
new file mode 100644
index 0000000000..dc88e36cef
--- /dev/null
+++ b/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js
@@ -0,0 +1,185 @@
+"use strict";
+
+const CHECK_DEFAULT_INITIAL = Services.prefs.getBoolPref(
+ "browser.shell.checkDefaultBrowser"
+);
+
+add_task(async function clicking_make_default_checks_alwaysCheck_checkbox() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+
+ await test_with_mock_shellservice({ isDefault: false }, async function () {
+ let checkDefaultBrowserState = isDefault => {
+ let isDefaultPane = content.document.getElementById("isDefaultPane");
+ let isNotDefaultPane =
+ content.document.getElementById("isNotDefaultPane");
+ Assert.equal(
+ ContentTaskUtils.is_hidden(isDefaultPane),
+ !isDefault,
+ "The 'browser is default' pane should be hidden when browser is not default"
+ );
+ Assert.equal(
+ ContentTaskUtils.is_hidden(isNotDefaultPane),
+ isDefault,
+ "The 'make default' pane should be hidden when browser is default"
+ );
+ };
+
+ checkDefaultBrowserState(false);
+
+ let alwaysCheck = content.document.getElementById("alwaysCheckDefault");
+ Assert.ok(!alwaysCheck.checked, "Always Check is unchecked by default");
+ Assert.ok(
+ !Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"),
+ "alwaysCheck pref should be false by default in test runs"
+ );
+
+ let setDefaultButton = content.document.getElementById("setDefaultButton");
+ setDefaultButton.click();
+ content.window.gMainPane.updateSetDefaultBrowser();
+
+ await ContentTaskUtils.waitForCondition(
+ () => alwaysCheck.checked,
+ "'Always Check' checkbox should get checked after clicking the 'Set Default' button"
+ );
+
+ Assert.ok(
+ alwaysCheck.checked,
+ "Clicking 'Make Default' checks the 'Always Check' checkbox"
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"),
+ "Checking the checkbox should set the pref to true"
+ );
+ Assert.ok(
+ alwaysCheck.disabled,
+ "'Always Check' checkbox is locked with default browser and alwaysCheck=true"
+ );
+ checkDefaultBrowserState(true);
+ Assert.ok(
+ Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"),
+ "checkDefaultBrowser pref is now enabled"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser");
+});
+
+add_task(async function clicking_make_default_checks_alwaysCheck_checkbox() {
+ Services.prefs.lockPref("browser.shell.checkDefaultBrowser");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+
+ await test_with_mock_shellservice({ isDefault: false }, async function () {
+ let isDefaultPane = content.document.getElementById("isDefaultPane");
+ let isNotDefaultPane = content.document.getElementById("isNotDefaultPane");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(isDefaultPane),
+ "The 'browser is default' pane should be hidden when not default"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_visible(isNotDefaultPane),
+ "The 'make default' pane should be visible when not default"
+ );
+
+ let alwaysCheck = content.document.getElementById("alwaysCheckDefault");
+ Assert.ok(alwaysCheck.disabled, "Always Check is disabled when locked");
+ Assert.ok(
+ alwaysCheck.checked,
+ "Always Check is checked because defaultPref is true and pref is locked"
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"),
+ "alwaysCheck pref should ship with 'true' by default"
+ );
+
+ let setDefaultButton = content.document.getElementById("setDefaultButton");
+ setDefaultButton.click();
+ content.window.gMainPane.updateSetDefaultBrowser();
+
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.is_visible(isDefaultPane),
+ "Browser is now default"
+ );
+
+ Assert.ok(
+ alwaysCheck.checked,
+ "'Always Check' is still checked because it's locked"
+ );
+ Assert.ok(
+ alwaysCheck.disabled,
+ "'Always Check is disabled because it's locked"
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"),
+ "The pref is locked and so doesn't get changed"
+ );
+ });
+
+ Services.prefs.unlockPref("browser.shell.checkDefaultBrowser");
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function make_default_disabled_until_prefs_are_loaded() {
+ // Testcase with Firefox not set as the default browser
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+ await test_with_mock_shellservice({ isDefault: false }, async function () {
+ let alwaysCheck = content.document.getElementById("alwaysCheckDefault");
+ Assert.ok(
+ !alwaysCheck.disabled,
+ "'Always Check' is enabled after default browser updated"
+ );
+ });
+ gBrowser.removeCurrentTab();
+
+ // Testcase with Firefox set as the default browser
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+ await test_with_mock_shellservice({ isDefault: true }, async function () {
+ let alwaysCheck = content.document.getElementById("alwaysCheckDefault");
+ Assert.ok(
+ alwaysCheck.disabled,
+ "'Always Check' is still disabled after default browser updated"
+ );
+ });
+ gBrowser.removeCurrentTab();
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.unlockPref("browser.shell.checkDefaultBrowser");
+ Services.prefs.setBoolPref(
+ "browser.shell.checkDefaultBrowser",
+ CHECK_DEFAULT_INITIAL
+ );
+});
+
+async function test_with_mock_shellservice(options, testFn) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [options],
+ async function (contentOptions) {
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.oldShellService = win.getShellService();
+ let mockShellService = {
+ _isDefault: false,
+ isDefaultBrowser() {
+ return this._isDefault;
+ },
+ setDefaultBrowser() {
+ this._isDefault = true;
+ },
+ };
+ win.getShellService = function () {
+ return mockShellService;
+ };
+ mockShellService._isDefault = contentOptions.isDefault;
+ win.gMainPane.updateSetDefaultBrowser();
+ }
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], testFn);
+
+ Services.prefs.setBoolPref(
+ "browser.shell.checkDefaultBrowser",
+ CHECK_DEFAULT_INITIAL
+ );
+}
diff --git a/browser/components/preferences/tests/browser_engines.js b/browser/components/preferences/tests/browser_engines.js
new file mode 100644
index 0000000000..aa681e7039
--- /dev/null
+++ b/browser/components/preferences/tests/browser_engines.js
@@ -0,0 +1,141 @@
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+function getCellText(tree, i, cellName) {
+ return tree.view.getCellText(i, tree.columns.getNamedColumn(cellName));
+}
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension({
+ keyword: ["testing", "customkeyword"],
+ search_url: "https://example.com/engine1",
+ search_url_get_params: "search={searchTerms}",
+ });
+});
+
+add_task(async function test_engine_list() {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneSearch", "Search pane is selected by default");
+ let doc = gBrowser.contentDocument;
+
+ let tree = doc.querySelector("#engineList");
+ ok(
+ !tree.hidden,
+ "The search engine list should be visible when Search is requested"
+ );
+
+ // Check for default search engines to be displayed in the engineList
+ let defaultEngines = await Services.search.getAppProvidedEngines();
+ for (let i = 0; i < defaultEngines.length; i++) {
+ let engine = defaultEngines[i];
+ is(
+ getCellText(tree, i, "engineName"),
+ engine.name,
+ "Default search engine " + engine.name + " displayed correctly"
+ );
+ }
+
+ let customEngineIndex = defaultEngines.length;
+ is(
+ getCellText(tree, customEngineIndex, "engineKeyword"),
+ "testing, customkeyword",
+ "Show internal aliases"
+ );
+
+ // Scroll the treeview into view since mouse operations
+ // off screen can act confusingly.
+ tree.scrollIntoView();
+ let rect = tree.getCoordsForCellItem(
+ customEngineIndex,
+ tree.columns.getNamedColumn("engineKeyword"),
+ "text"
+ );
+ let x = rect.x + rect.width / 2;
+ let y = rect.y + rect.height / 2;
+ let win = tree.ownerGlobal;
+
+ let promise = BrowserTestUtils.waitForEvent(tree, "dblclick");
+ EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win);
+ EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win);
+ await promise;
+
+ EventUtils.sendString("newkeyword");
+ EventUtils.sendKey("RETURN");
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ getCellText(tree, customEngineIndex, "engineKeyword") ===
+ "newkeyword, testing, customkeyword"
+ );
+ });
+
+ // Avoid duplicated keywords
+ tree.view.setCellText(
+ 0,
+ tree.columns.getNamedColumn("engineKeyword"),
+ "keyword"
+ );
+ tree.view.setCellText(
+ 1,
+ tree.columns.getNamedColumn("engineKeyword"),
+ "keyword"
+ );
+ isnot(
+ getCellText(tree, 1, "engineKeyword"),
+ "keyword",
+ "Do not allow duplicated keywords"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_remove_button_disabled_state() {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneSearch", "Search pane is selected by default");
+ let doc = gBrowser.contentDocument;
+
+ let tree = doc.querySelector("#engineList");
+ ok(
+ !tree.hidden,
+ "The search engine list should be visible when Search is requested"
+ );
+
+ let defaultEngines = await Services.search.getAppProvidedEngines();
+ for (let i = 0; i < defaultEngines.length; i++) {
+ let engine = defaultEngines[i];
+
+ let isDefaultSearchEngine =
+ engine.name == Services.search.defaultEngine.name ||
+ engine.name == Services.search.defaultPrivateEngine.name;
+
+ tree.scrollIntoView();
+ let rect = tree.getCoordsForCellItem(
+ i,
+ tree.columns.getNamedColumn("engineName"),
+ "text"
+ );
+ let x = rect.x + rect.width / 2;
+ let y = rect.y + rect.height / 2;
+ let win = tree.ownerGlobal;
+
+ let promise = BrowserTestUtils.waitForEvent(tree, "click");
+ EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win);
+ await promise;
+
+ let removeButton = doc.querySelector("#removeEngineButton");
+ is(
+ removeButton.disabled,
+ isDefaultSearchEngine,
+ "Remove button is in correct disable state"
+ );
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_etp_exceptions_dialog.js b/browser/components/preferences/tests/browser_etp_exceptions_dialog.js
new file mode 100644
index 0000000000..349223995c
--- /dev/null
+++ b/browser/components/preferences/tests/browser_etp_exceptions_dialog.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml";
+
+const TRACKING_URL = "https://example.com";
+
+async function openETPExceptionsDialog(doc) {
+ let exceptionsButton = doc.getElementById("trackingProtectionExceptions");
+ ok(exceptionsButton, "trackingProtectionExceptions button found");
+ let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL);
+ exceptionsButton.click();
+ let dialog = await dialogPromise;
+ return dialog;
+}
+
+async function addETPPermission(doc) {
+ let dialog = await openETPExceptionsDialog(doc);
+ let url = dialog.document.getElementById("url");
+ let buttonDisableETP = dialog.document.getElementById("btnDisableETP");
+ let permissionsBox = dialog.document.getElementById("permissionsBox");
+ let currentPermissions = permissionsBox.itemCount;
+
+ url.value = TRACKING_URL;
+ url.dispatchEvent(new Event("input", { bubbles: true }));
+ is(
+ buttonDisableETP.hasAttribute("disabled"),
+ false,
+ "Disable ETP button is selectable after url is entered"
+ );
+ buttonDisableETP.click();
+
+ // Website is listed
+ is(
+ permissionsBox.itemCount,
+ currentPermissions + 1,
+ "Website added in url should be in the list"
+ );
+ let saveButton = dialog.document.querySelector("dialog").getButton("accept");
+ saveButton.click();
+ BrowserTestUtils.waitForEvent(dialog, "unload");
+}
+
+async function removeETPPermission(doc) {
+ let dialog = await openETPExceptionsDialog(doc);
+ let permissionsBox = dialog.document.getElementById("permissionsBox");
+ let elements = permissionsBox.getElementsByAttribute("origin", TRACKING_URL);
+ // Website is listed
+ ok(permissionsBox.itemCount, "List is not empty");
+ permissionsBox.selectItem(elements[0]);
+ let removePermissionButton =
+ dialog.document.getElementById("removePermission");
+ is(
+ removePermissionButton.hasAttribute("disabled"),
+ false,
+ "The button should be clickable to remove selected item"
+ );
+ removePermissionButton.click();
+
+ let saveButton = dialog.document.querySelector("dialog").getButton("accept");
+ saveButton.click();
+ BrowserTestUtils.waitForEvent(dialog, "unload");
+}
+
+async function checkShieldIcon(shieldIcon) {
+ // Open the website and check that the tracking protection icon is enabled/disabled
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TRACKING_URL);
+ let icon = document.getElementById("tracking-protection-icon");
+ is(
+ gBrowser.ownerGlobal
+ .getComputedStyle(icon)
+ .getPropertyValue("list-style-image"),
+ shieldIcon,
+ `The tracking protection icon shows the icon ${shieldIcon}`
+ );
+ BrowserTestUtils.removeTab(tab);
+}
+
+// test adds and removes an ETP permission via the about:preferences#privacy and checks if the ProtectionsUI shield icon resembles the state
+add_task(async function ETPPermissionSyncedFromPrivacyPane() {
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = win.document;
+ await addETPPermission(doc);
+ await checkShieldIcon(
+ `url("chrome://browser/skin/tracking-protection-disabled.svg")`
+ );
+ await removeETPPermission(doc);
+ await checkShieldIcon(`url("chrome://browser/skin/tracking-protection.svg")`);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_experimental_features.js b/browser/components/preferences/tests/browser_experimental_features.js
new file mode 100644
index 0000000000..cecbb60893
--- /dev/null
+++ b/browser/components/preferences/tests/browser_experimental_features.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testPrefRequired() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.experimental", false]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let experimentalCategory = doc.getElementById("category-experimental");
+ ok(experimentalCategory, "The category exists");
+ ok(experimentalCategory.hidden, "The category is hidden");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testCanOpenWithPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.experimental", true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let experimentalCategory = doc.getElementById("category-experimental");
+ ok(experimentalCategory, "The category exists");
+ ok(!experimentalCategory.hidden, "The category is not hidden");
+
+ let categoryHeader = await TestUtils.waitForCondition(
+ () => doc.getElementById("firefoxExperimentalCategory"),
+ "Waiting for experimental features category to get initialized"
+ );
+ ok(
+ categoryHeader.hidden,
+ "The category header should be hidden when Home is selected"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(experimentalCategory, {}, doc.ownerGlobal);
+ await TestUtils.waitForCondition(
+ () => !categoryHeader.hidden,
+ "Waiting until category is visible"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testSearchFindsExperiments() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.experimental", true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+
+ let experimentalCategory = doc.getElementById("category-experimental");
+ ok(experimentalCategory, "The category exists");
+ ok(!experimentalCategory.hidden, "The category is not hidden");
+
+ await TestUtils.waitForCondition(
+ () => doc.getElementById("firefoxExperimentalCategory"),
+ "Waiting for experimental features category to get initialized"
+ );
+ await evaluateSearchResults(
+ "advanced configuration",
+ ["pane-experimental-featureGates"],
+ /* include experiments */ true
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_experimental_features_filter.js b/browser/components/preferences/tests/browser_experimental_features_filter.js
new file mode 100644
index 0000000000..6bd66db555
--- /dev/null
+++ b/browser/components/preferences/tests/browser_experimental_features_filter.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test verifies that searching filters the features to just that subset that
+// contains the search terms.
+add_task(async function testFilterFeatures() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.experimental", true]],
+ });
+
+ // Add a number of test features.
+ const server = new DefinitionServer();
+ let definitions = [
+ {
+ id: "test-featureA",
+ preference: "test.featureA",
+ title: "Experimental Feature 1",
+ description: "This is a fun experimental feature you can enable",
+ result: true,
+ },
+ {
+ id: "test-featureB",
+ preference: "test.featureB",
+ title: "Experimental Thing 2",
+ description: "This is a very boring experimental tool",
+ result: false,
+ },
+ {
+ id: "test-featureC",
+ preference: "test.featureC",
+ title: "Experimental Thing 3",
+ description: "This is a fun experimental feature for you can enable",
+ result: true,
+ },
+ {
+ id: "test-featureD",
+ preference: "test.featureD",
+ title: "Experimental Thing 4",
+ description: "This is a not a checkbox that you should be enabling",
+ result: false,
+ },
+ ];
+ for (let { id, preference } of definitions) {
+ server.addDefinition({ id, preference, isPublic: true });
+ }
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `about:preferences?definitionsUrl=${encodeURIComponent(
+ server.definitionsUrl
+ )}#paneExperimental`
+ );
+ let doc = gBrowser.contentDocument;
+
+ await TestUtils.waitForCondition(
+ () => doc.getElementById(definitions[definitions.length - 1].id),
+ "wait for the first public feature to get added to the DOM"
+ );
+
+ // Manually modify the labels of the features that were just added, so that the test
+ // can rely on consistent search terms.
+ for (let definition of definitions) {
+ let mainItem = doc.getElementById(definition.id);
+ mainItem.label = definition.title;
+ mainItem.removeAttribute("data-l10n-id");
+ let descItem = doc.getElementById(definition.id + "-description");
+ descItem.textContent = definition.description;
+ descItem.removeAttribute("data-l10n-id");
+ }
+
+ // First, check that all of the items are visible by default.
+ for (let definition of definitions) {
+ checkVisibility(
+ doc.getElementById(definition.id),
+ true,
+ `${definition.id} should be initially visible`
+ );
+ }
+
+ // After searching, only a subset should be visible.
+ await enterSearch(doc, "feature");
+
+ for (let definition of definitions) {
+ checkVisibility(
+ doc.getElementById(definition.id),
+ definition.result,
+ `${definition.id} should be ${
+ definition.result ? "visible" : "hidden"
+ } after first search`
+ );
+ info("Text for item was: " + doc.getElementById(definition.id).textContent);
+ }
+
+ // Further restrict the search to only a single item.
+ await enterSearch(doc, " you");
+
+ let shouldBeVisible = true;
+ for (let definition of definitions) {
+ checkVisibility(
+ doc.getElementById(definition.id),
+ shouldBeVisible,
+ `${definition.id} should be ${
+ shouldBeVisible ? "visible" : "hidden"
+ } after further search`
+ );
+ shouldBeVisible = false;
+ }
+
+ // Reset the search entirely.
+ let searchInput = doc.getElementById("searchInput");
+ searchInput.value = "";
+ searchInput.doCommand();
+
+ // Clearing the search will go to the general pane so switch back to the experimental pane.
+ EventUtils.synthesizeMouseAtCenter(
+ doc.getElementById("category-experimental"),
+ {},
+ gBrowser.contentWindow
+ );
+
+ for (let definition of definitions) {
+ checkVisibility(
+ doc.getElementById(definition.id),
+ true,
+ `${definition.id} should be visible after search cleared`
+ );
+ }
+
+ // Simulate entering a search and then clicking one of the category labels. The search
+ // should reset each time.
+ for (let category of ["category-search", "category-experimental"]) {
+ await enterSearch(doc, "feature");
+
+ for (let definition of definitions) {
+ checkVisibility(
+ doc.getElementById(definition.id),
+ definition.result,
+ `${definition.id} should be ${
+ definition.result ? "visible" : "hidden"
+ } after next search`
+ );
+ }
+
+ EventUtils.synthesizeMouseAtCenter(
+ doc.getElementById(category),
+ {},
+ gBrowser.contentWindow
+ );
+
+ for (let definition of definitions) {
+ checkVisibility(
+ doc.getElementById(definition.id),
+ true,
+ `${definition.id} should be visible after category change to ${category}`
+ );
+ }
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+function checkVisibility(element, expected, desc) {
+ return expected
+ ? is_element_visible(element, desc)
+ : is_element_hidden(element, desc);
+}
+
+function enterSearch(doc, query) {
+ let searchInput = doc.getElementById("searchInput");
+ searchInput.focus();
+
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+
+ EventUtils.sendString(query);
+
+ return searchCompletedPromise;
+}
diff --git a/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js b/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js
new file mode 100644
index 0000000000..e1e2adced9
--- /dev/null
+++ b/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testNonPublicFeaturesShouldntGetDisplayed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.experimental", true]],
+ });
+
+ const server = new DefinitionServer();
+ let definitions = [
+ { id: "test-featureA", isPublic: true, preference: "test.feature.a" },
+ { id: "test-featureB", isPublic: false, preference: "test.feature.b" },
+ { id: "test-featureC", isPublic: true, preference: "test.feature.c" },
+ ];
+ for (let { id, isPublic, preference } of definitions) {
+ server.addDefinition({ id, isPublic, preference });
+ }
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `about:preferences?definitionsUrl=${encodeURIComponent(
+ server.definitionsUrl
+ )}#paneExperimental`
+ );
+ let doc = gBrowser.contentDocument;
+
+ await TestUtils.waitForCondition(
+ () => doc.getElementById(definitions.find(d => d.isPublic).id),
+ "wait for the first public feature to get added to the DOM"
+ );
+
+ for (let definition of definitions) {
+ is(
+ !!doc.getElementById(definition.id),
+ definition.isPublic,
+ "feature should only be in DOM if it's public: " + definition.id
+ );
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testNonPublicFeaturesShouldntGetDisplayed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.preferences.experimental", true],
+ ["browser.preferences.experimental.hidden", false],
+ ],
+ });
+
+ const server = new DefinitionServer();
+ server.addDefinition({
+ id: "test-hidden",
+ isPublic: false,
+ preference: "test.feature.hidden",
+ });
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `about:preferences?definitionsUrl=${encodeURIComponent(
+ server.definitionsUrl
+ )}#paneExperimental`
+ );
+ let doc = gBrowser.contentDocument;
+
+ await TestUtils.waitForCondition(
+ () => doc.getElementById("category-experimental").hidden,
+ "Wait for Experimental Features section to get hidden"
+ );
+
+ ok(
+ doc.getElementById("category-experimental").hidden,
+ "Experimental Features section should be hidden when all features are hidden"
+ );
+ ok(
+ !doc.getElementById("firefoxExperimentalCategory"),
+ "Experimental Features header should not exist when all features are hidden"
+ );
+ is(
+ doc.querySelector(".category[selected]").id,
+ "category-general",
+ "When the experimental features section is hidden, navigating to #experimental should redirect to #general"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_experimental_features_resetall.js b/browser/components/preferences/tests/browser_experimental_features_resetall.js
new file mode 100644
index 0000000000..636374c057
--- /dev/null
+++ b/browser/components/preferences/tests/browser_experimental_features_resetall.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// It doesn't matter what two preferences are used here, as long as the first is a built-in
+// one that defaults to false and the second defaults to true.
+const KNOWN_PREF_1 = "browser.display.use_system_colors";
+const KNOWN_PREF_2 = "browser.underline_anchors";
+
+// This test verifies that pressing the reset all button for experimental features
+// resets all of the checkboxes to their default state.
+add_task(async function testResetAll() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.preferences.experimental", true],
+ ["test.featureA", false],
+ ["test.featureB", true],
+ [KNOWN_PREF_1, false],
+ [KNOWN_PREF_2, true],
+ ],
+ });
+
+ // Add a number of test features.
+ const server = new DefinitionServer();
+ let definitions = [
+ {
+ id: "test-featureA",
+ preference: "test.featureA",
+ defaultValue: false,
+ },
+ {
+ id: "test-featureB",
+ preference: "test.featureB",
+ defaultValue: true,
+ },
+ {
+ id: "test-featureC",
+ preference: KNOWN_PREF_1,
+ defaultValue: false,
+ },
+ {
+ id: "test-featureD",
+ preference: KNOWN_PREF_2,
+ defaultValue: true,
+ },
+ ];
+ for (let { id, preference, defaultValue } of definitions) {
+ server.addDefinition({ id, preference, defaultValue, isPublic: true });
+ }
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `about:preferences?definitionsUrl=${encodeURIComponent(
+ server.definitionsUrl
+ )}#paneExperimental`
+ );
+ let doc = gBrowser.contentDocument;
+
+ await TestUtils.waitForCondition(
+ () => doc.getElementById(definitions[definitions.length - 1].id),
+ "wait for the first public feature to get added to the DOM"
+ );
+
+ // Check the initial state of each feature.
+ ok(!Services.prefs.getBoolPref("test.featureA"), "initial state A");
+ ok(Services.prefs.getBoolPref("test.featureB"), "initial state B");
+ ok(!Services.prefs.getBoolPref(KNOWN_PREF_1), "initial state C");
+ ok(Services.prefs.getBoolPref(KNOWN_PREF_2), "initial state D");
+
+ // Modify the state of some of the features.
+ doc.getElementById("test-featureC").click();
+ doc.getElementById("test-featureD").click();
+ ok(!Services.prefs.getBoolPref("test.featureA"), "modified state A");
+ ok(Services.prefs.getBoolPref("test.featureB"), "modified state B");
+ ok(Services.prefs.getBoolPref(KNOWN_PREF_1), "modified state C");
+ ok(!Services.prefs.getBoolPref(KNOWN_PREF_2), "modified state D");
+
+ // State after reset.
+ let prefChangedPromise = new Promise(resolve => {
+ Services.prefs.addObserver(KNOWN_PREF_2, function observer() {
+ Services.prefs.removeObserver(KNOWN_PREF_2, observer);
+ resolve();
+ });
+ });
+ doc.getElementById("experimentalCategory-reset").click();
+ await prefChangedPromise;
+
+ // The preferences will be reset to the default value for the feature.
+ ok(!Services.prefs.getBoolPref("test.featureA"), "after reset state A");
+ ok(Services.prefs.getBoolPref("test.featureB"), "after reset state B");
+ ok(!Services.prefs.getBoolPref(KNOWN_PREF_1), "after reset state C");
+ ok(Services.prefs.getBoolPref(KNOWN_PREF_2), "after reset state D");
+ ok(
+ !doc.getElementById("test-featureA").checked,
+ "after reset checkbox state A"
+ );
+ ok(
+ doc.getElementById("test-featureB").checked,
+ "after reset checkbox state B"
+ );
+ ok(
+ !doc.getElementById("test-featureC").checked,
+ "after reset checkbox state C"
+ );
+ ok(
+ doc.getElementById("test-featureD").checked,
+ "after reset checkbox state D"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_extension_controlled.js b/browser/components/preferences/tests/browser_extension_controlled.js
new file mode 100644
index 0000000000..6cec9ba93a
--- /dev/null
+++ b/browser/components/preferences/tests/browser_extension_controlled.js
@@ -0,0 +1,1447 @@
+/* eslint-env webextensions */
+
+const PROXY_PREF = "network.proxy.type";
+const HOMEPAGE_URL_PREF = "browser.startup.homepage";
+const HOMEPAGE_OVERRIDE_KEY = "homepage_override";
+const URL_OVERRIDES_TYPE = "url_overrides";
+const NEW_TAB_KEY = "newTabURL";
+const PREF_SETTING_TYPE = "prefs";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AboutNewTab",
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "proxyType", PROXY_PREF);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+AddonTestUtils.initMochitest(this);
+
+const { ExtensionPreferencesManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPreferencesManager.sys.mjs"
+);
+
+const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+const CHROME_URL_ROOT = TEST_DIR + "/";
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml";
+let sitePermissionsDialog;
+
+function getSupportsFile(path) {
+ let cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+ let uri = Services.io.newURI(CHROME_URL_ROOT + path);
+ let fileurl = cr.convertChromeURL(uri);
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+}
+
+function waitForMessageChange(
+ element,
+ cb,
+ opts = { attributes: true, attributeFilter: ["hidden"] }
+) {
+ return waitForMutation(element, opts, cb);
+}
+
+function getElement(id, doc = gBrowser.contentDocument) {
+ return doc.getElementById(id);
+}
+
+function waitForMessageHidden(messageId, doc) {
+ return waitForMessageChange(
+ getElement(messageId, doc),
+ target => target.hidden
+ );
+}
+
+function waitForMessageShown(messageId, doc) {
+ return waitForMessageChange(
+ getElement(messageId, doc),
+ target => !target.hidden
+ );
+}
+
+function waitForEnableMessage(messageId, doc) {
+ return waitForMessageChange(
+ getElement(messageId, doc),
+ target => target.classList.contains("extension-controlled-disabled"),
+ { attributeFilter: ["class"], attributes: true }
+ );
+}
+
+function waitForMessageContent(messageId, l10nId, doc) {
+ return waitForMessageChange(
+ getElement(messageId, doc),
+ target => doc.l10n.getAttributes(target).id === l10nId,
+ { childList: true }
+ );
+}
+
+async function openNotificationsPermissionDialog() {
+ let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let doc = content.document;
+ let settingsButton = doc.getElementById("notificationSettingsButton");
+ settingsButton.click();
+ });
+
+ sitePermissionsDialog = await dialogOpened;
+ await sitePermissionsDialog.document.mozSubdialogReady;
+}
+
+async function disableExtensionViaClick(labelId, disableButtonId, doc) {
+ let controlledLabel = doc.getElementById(labelId);
+
+ let enableMessageShown = waitForEnableMessage(labelId, doc);
+ doc.getElementById(disableButtonId).click();
+ await enableMessageShown;
+
+ let controlledDescription = controlledLabel.querySelector("description");
+ is(
+ doc.l10n.getAttributes(controlledDescription.querySelector("label")).id,
+ "extension-controlled-enable",
+ "The user is notified of how to enable the extension again."
+ );
+
+ // The user can dismiss the enable instructions.
+ let hidden = waitForMessageHidden(labelId, doc);
+ controlledLabel.querySelector("image:last-of-type").click();
+ await hidden;
+}
+
+async function reEnableExtension(addon, labelId) {
+ let controlledMessageShown = waitForMessageShown(labelId);
+ await addon.enable();
+ await controlledMessageShown;
+}
+
+add_task(async function testExtensionControlledHomepage() {
+ const ADDON_ID = "@set_homepage";
+ const SECOND_ADDON_ID = "@second_set_homepage";
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ let homepagePref = () => Services.prefs.getCharPref(HOMEPAGE_URL_PREF);
+ let originalHomepagePref = homepagePref();
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#home",
+ "#home should be in the URI for about:preferences"
+ );
+ let doc = gBrowser.contentDocument;
+ let homeModeEl = doc.getElementById("homeMode");
+ let customSettingsSection = doc.getElementById("customSettings");
+
+ is(homeModeEl.itemCount, 3, "The menu list starts with 3 options");
+
+ let promise = TestUtils.waitForCondition(
+ () => homeModeEl.itemCount === 4,
+ "wait for the addon option to be added as an option in the menu list"
+ );
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ name: "set_homepage",
+ browser_specific_settings: {
+ gecko: {
+ id: ADDON_ID,
+ },
+ },
+ chrome_settings_overrides: { homepage: "/home.html" },
+ },
+ });
+ await extension.startup();
+ await promise;
+
+ // The homepage is set to the default and the custom settings section is hidden
+ is(homeModeEl.disabled, false, "The homepage menulist is enabled");
+ is(
+ customSettingsSection.hidden,
+ true,
+ "The custom settings element is hidden"
+ );
+
+ let addon = await AddonManager.getAddonByID(ADDON_ID);
+ is(
+ homeModeEl.value,
+ addon.id,
+ "the home select menu's value is set to the addon"
+ );
+
+ promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF);
+ // Set the Menu to the default value
+ homeModeEl.value = "0";
+ homeModeEl.dispatchEvent(new Event("command"));
+ await promise;
+ is(homepagePref(), originalHomepagePref, "homepage is set back to default");
+ let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ addon.id,
+ HOMEPAGE_OVERRIDE_KEY,
+ PREF_SETTING_TYPE
+ );
+ is(
+ levelOfControl,
+ "not_controllable",
+ "getLevelOfControl returns not_controllable."
+ );
+ let setting = await ExtensionPreferencesManager.getSetting(
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ ok(!setting.value, "the setting is not set.");
+
+ promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF);
+ // Set the menu to the addon value
+ homeModeEl.value = ADDON_ID;
+ homeModeEl.dispatchEvent(new Event("command"));
+ await promise;
+ ok(
+ homepagePref().startsWith("moz-extension") &&
+ homepagePref().endsWith("home.html"),
+ "Home url should be provided by the extension."
+ );
+ levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ addon.id,
+ HOMEPAGE_OVERRIDE_KEY,
+ PREF_SETTING_TYPE
+ );
+ is(
+ levelOfControl,
+ "controlled_by_this_extension",
+ "getLevelOfControl returns controlled_by_this_extension."
+ );
+ setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY);
+ ok(
+ setting.value.startsWith("moz-extension") &&
+ setting.value.endsWith("home.html"),
+ "The setting value is the same as the extension."
+ );
+
+ // Add a second extension, ensure it is added to the menulist and selected.
+ promise = TestUtils.waitForCondition(
+ () => homeModeEl.itemCount == 5,
+ "addon option is added as an option in the menu list"
+ );
+ let secondExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ name: "second_set_homepage",
+ browser_specific_settings: {
+ gecko: {
+ id: SECOND_ADDON_ID,
+ },
+ },
+ chrome_settings_overrides: { homepage: "/home2.html" },
+ },
+ });
+ await secondExtension.startup();
+ await promise;
+
+ let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID);
+ is(homeModeEl.value, SECOND_ADDON_ID, "home menulist is set to the add-on");
+
+ levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ secondAddon.id,
+ HOMEPAGE_OVERRIDE_KEY,
+ PREF_SETTING_TYPE
+ );
+ is(
+ levelOfControl,
+ "controlled_by_this_extension",
+ "getLevelOfControl returns controlled_by_this_extension."
+ );
+ setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY);
+ ok(
+ setting.value.startsWith("moz-extension") &&
+ setting.value.endsWith("home2.html"),
+ "The setting value is the same as the extension."
+ );
+
+ promise = TestUtils.waitForCondition(
+ () => homeModeEl.itemCount == 4,
+ "addon option is no longer an option in the menu list after disable, even if it was not selected"
+ );
+ await addon.disable();
+ await promise;
+
+ // Ensure that re-enabling an addon adds it back to the menulist
+ promise = TestUtils.waitForCondition(
+ () => homeModeEl.itemCount == 5,
+ "addon option is added again to the menulist when enabled"
+ );
+ await addon.enable();
+ await promise;
+
+ promise = TestUtils.waitForCondition(
+ () => homeModeEl.itemCount == 4,
+ "addon option is no longer an option in the menu list after disable"
+ );
+ await secondAddon.disable();
+ await promise;
+
+ promise = TestUtils.waitForCondition(
+ () => homeModeEl.itemCount == 5,
+ "addon option is added again to the menulist when enabled"
+ );
+ await secondAddon.enable();
+ await promise;
+
+ promise = TestUtils.waitForCondition(
+ () => homeModeEl.itemCount == 3,
+ "addon options are no longer an option in the menu list after disabling all addons"
+ );
+ await secondAddon.disable();
+ await addon.disable();
+ await promise;
+
+ is(homeModeEl.value, "0", "addon option is not selected in the menu list");
+
+ levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ secondAddon.id,
+ HOMEPAGE_OVERRIDE_KEY,
+ PREF_SETTING_TYPE
+ );
+ is(
+ levelOfControl,
+ "controllable_by_this_extension",
+ "getLevelOfControl returns controllable_by_this_extension."
+ );
+ setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY);
+ ok(!setting.value, "The setting value is back to default.");
+
+ // The homepage elements are reset to their original state.
+ is(homepagePref(), originalHomepagePref, "homepage is set back to default");
+ is(homeModeEl.disabled, false, "The homepage menulist is enabled");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await extension.unload();
+ await secondExtension.unload();
+});
+
+add_task(async function testPrefLockedHomepage() {
+ const ADDON_ID = "@set_homepage";
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#home",
+ "#home should be in the URI for about:preferences"
+ );
+
+ let homePagePref = "browser.startup.homepage";
+ let buttonPrefs = [
+ "pref.browser.homepage.disable_button.current_page",
+ "pref.browser.homepage.disable_button.bookmark_page",
+ "pref.browser.homepage.disable_button.restore_default",
+ ];
+ let homeModeEl = doc.getElementById("homeMode");
+ let homePageInput = doc.getElementById("homePageUrl");
+ let prefs = Services.prefs.getDefaultBranch(null);
+ let mutationOpts = { attributes: true, attributeFilter: ["disabled"] };
+
+ // Helper functions.
+ let getButton = pref =>
+ doc.querySelector(`.homepage-button[preference="${pref}"`);
+ let waitForAllMutations = () =>
+ Promise.all(
+ buttonPrefs
+ .map(pref => waitForMutation(getButton(pref), mutationOpts))
+ .concat([
+ waitForMutation(homeModeEl, mutationOpts),
+ waitForMutation(homePageInput, mutationOpts),
+ ])
+ );
+ let getHomepage = () =>
+ Services.prefs.getCharPref("browser.startup.homepage");
+
+ let originalHomepage = getHomepage();
+ let extensionHomepage = "https://developer.mozilla.org/";
+ let lockedHomepage = "http://www.yahoo.com";
+
+ let lockPrefs = () => {
+ buttonPrefs.forEach(pref => {
+ prefs.setBoolPref(pref, true);
+ prefs.lockPref(pref);
+ });
+ // Do the homepage last since that's the only pref that triggers a UI update.
+ prefs.setCharPref(homePagePref, lockedHomepage);
+ prefs.lockPref(homePagePref);
+ };
+ let unlockPrefs = () => {
+ buttonPrefs.forEach(pref => {
+ prefs.unlockPref(pref);
+ prefs.setBoolPref(pref, false);
+ });
+ // Do the homepage last since that's the only pref that triggers a UI update.
+ prefs.unlockPref(homePagePref);
+ prefs.setCharPref(homePagePref, originalHomepage);
+ };
+
+ // Lock or unlock prefs then wait for all mutations to finish.
+ // Expects a bool indicating if we should lock or unlock.
+ let waitForLockMutations = lock => {
+ let mutationsDone = waitForAllMutations();
+ if (lock) {
+ lockPrefs();
+ } else {
+ unlockPrefs();
+ }
+ return mutationsDone;
+ };
+
+ ok(
+ originalHomepage != extensionHomepage,
+ "The extension will change the homepage"
+ );
+
+ // Install an extension that sets the homepage to MDN.
+ let promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF);
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ name: "set_homepage",
+ browser_specific_settings: {
+ gecko: {
+ id: ADDON_ID,
+ },
+ },
+ chrome_settings_overrides: { homepage: "https://developer.mozilla.org/" },
+ },
+ });
+ await extension.startup();
+ await promise;
+
+ // Check that everything is still disabled, homepage didn't change.
+ is(
+ getHomepage(),
+ extensionHomepage,
+ "The reported homepage is set by the extension"
+ );
+ is(
+ homePageInput.value,
+ extensionHomepage,
+ "The homepage is set by the extension"
+ );
+
+ // Lock all of the prefs, wait for the UI to update.
+ await waitForLockMutations(true);
+
+ // Check that everything is now disabled.
+ is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref");
+ is(homePageInput.value, lockedHomepage, "The homepage is set by the pref");
+ is(
+ homePageInput.disabled,
+ true,
+ "The homepage is disabed when the pref is locked"
+ );
+
+ buttonPrefs.forEach(pref => {
+ is(
+ getButton(pref).disabled,
+ true,
+ `The ${pref} button is disabled when locked`
+ );
+ });
+
+ let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ ADDON_ID,
+ HOMEPAGE_OVERRIDE_KEY,
+ PREF_SETTING_TYPE
+ );
+ is(
+ levelOfControl,
+ "not_controllable",
+ "getLevelOfControl returns not_controllable, the pref is locked."
+ );
+
+ // Verify that the UI is selecting the extension's Id in the menulist
+ let unlockedPromise = TestUtils.waitForCondition(
+ () => homeModeEl.value == ADDON_ID,
+ "Homepage menulist value is equal to the extension ID"
+ );
+ // Unlock the prefs, wait for the UI to update.
+ unlockPrefs();
+ await unlockedPromise;
+
+ is(
+ homeModeEl.disabled,
+ false,
+ "the home select element is not disabled when the pref is not locked"
+ );
+ is(
+ homePageInput.disabled,
+ false,
+ "The homepage is enabled when the pref is unlocked"
+ );
+ is(
+ getHomepage(),
+ extensionHomepage,
+ "The homepage is reset to extension page"
+ );
+
+ levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ ADDON_ID,
+ HOMEPAGE_OVERRIDE_KEY,
+ PREF_SETTING_TYPE
+ );
+ is(
+ levelOfControl,
+ "controlled_by_this_extension",
+ "getLevelOfControl returns controlled_by_this_extension after prefs are unlocked."
+ );
+ let setting = await ExtensionPreferencesManager.getSetting(
+ HOMEPAGE_OVERRIDE_KEY
+ );
+ is(
+ setting.value,
+ extensionHomepage,
+ "The setting value is equal to the extensionHomepage."
+ );
+
+ // Uninstall the add-on.
+ promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF);
+ await extension.unload();
+ await promise;
+
+ setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY);
+ ok(!setting, "The setting is gone after the addon is uninstalled.");
+
+ // Check that everything is now enabled again.
+ is(
+ getHomepage(),
+ originalHomepage,
+ "The reported homepage is reset to original value"
+ );
+ is(homePageInput.value, "", "The homepage is empty");
+ is(
+ homePageInput.disabled,
+ false,
+ "The homepage is enabled after clearing lock"
+ );
+ is(
+ homeModeEl.disabled,
+ false,
+ "Homepage menulist is enabled after clearing lock"
+ );
+ buttonPrefs.forEach(pref => {
+ is(
+ getButton(pref).disabled,
+ false,
+ `The ${pref} button is enabled when unlocked`
+ );
+ });
+
+ // Lock the prefs without an extension.
+ await waitForLockMutations(true);
+
+ // Check that everything is now disabled.
+ is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref");
+ is(homePageInput.value, lockedHomepage, "The homepage is set by the pref");
+ is(
+ homePageInput.disabled,
+ true,
+ "The homepage is disabed when the pref is locked"
+ );
+ is(
+ homeModeEl.disabled,
+ true,
+ "Homepage menulist is disabled when pref is locked"
+ );
+ buttonPrefs.forEach(pref => {
+ is(
+ getButton(pref).disabled,
+ true,
+ `The ${pref} button is disabled when locked`
+ );
+ });
+
+ // Unlock the prefs without an extension.
+ await waitForLockMutations(false);
+
+ // Check that everything is enabled again.
+ is(
+ getHomepage(),
+ originalHomepage,
+ "The homepage is reset to the original value"
+ );
+ is(homePageInput.value, "", "The homepage is clear after being unlocked");
+ is(
+ homePageInput.disabled,
+ false,
+ "The homepage is enabled after clearing lock"
+ );
+ is(
+ homeModeEl.disabled,
+ false,
+ "Homepage menulist is enabled after clearing lock"
+ );
+ buttonPrefs.forEach(pref => {
+ is(
+ getButton(pref).disabled,
+ false,
+ `The ${pref} button is enabled when unlocked`
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testExtensionControlledNewTab() {
+ const ADDON_ID = "@set_newtab";
+ const SECOND_ADDON_ID = "@second_set_newtab";
+ const DEFAULT_NEWTAB = "about:newtab";
+ const NEWTAB_CONTROLLED_PREF = "browser.newtab.extensionControlled";
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#home",
+ "#home should be in the URI for about:preferences"
+ );
+
+ let doc = gBrowser.contentDocument;
+ let newTabMenuList = doc.getElementById("newTabMode");
+ // The new tab page is set to the default.
+ is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default");
+
+ let promise = TestUtils.waitForCondition(
+ () => newTabMenuList.itemCount == 3,
+ "addon option is added as an option in the menu list"
+ );
+ // Install an extension that will set the new tab page.
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ name: "set_newtab",
+ browser_specific_settings: {
+ gecko: {
+ id: ADDON_ID,
+ },
+ },
+ chrome_url_overrides: { newtab: "/newtab.html" },
+ },
+ });
+ await extension.startup();
+
+ await promise;
+ let addon = await AddonManager.getAddonByID(ADDON_ID);
+
+ is(newTabMenuList.value, ADDON_ID, "New tab menulist is set to the add-on");
+
+ let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ addon.id,
+ NEW_TAB_KEY,
+ URL_OVERRIDES_TYPE
+ );
+ is(
+ levelOfControl,
+ "controlled_by_this_extension",
+ "getLevelOfControl returns controlled_by_this_extension."
+ );
+ let setting = ExtensionSettingsStore.getSetting(
+ URL_OVERRIDES_TYPE,
+ NEW_TAB_KEY
+ );
+ ok(
+ setting.value.startsWith("moz-extension") &&
+ setting.value.endsWith("newtab.html"),
+ "The url_overrides is set by this extension"
+ );
+
+ promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF);
+ // Set the menu to the default value
+ newTabMenuList.value = "0";
+ newTabMenuList.dispatchEvent(new Event("command"));
+ await promise;
+ let newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false);
+ is(newTabPref, false, "the new tab is not controlled");
+
+ levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ addon.id,
+ NEW_TAB_KEY,
+ URL_OVERRIDES_TYPE
+ );
+ is(
+ levelOfControl,
+ "not_controllable",
+ "getLevelOfControl returns not_controllable."
+ );
+ setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY);
+ ok(!setting.value, "The url_overrides is not set by this extension");
+
+ promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF);
+ // Set the menu to a the addon value
+ newTabMenuList.value = ADDON_ID;
+ newTabMenuList.dispatchEvent(new Event("command"));
+ await promise;
+ newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false);
+ is(newTabPref, true, "the new tab is controlled");
+
+ // Add a second extension, ensure it is added to the menulist and selected.
+ promise = TestUtils.waitForCondition(
+ () => newTabMenuList.itemCount == 4,
+ "addon option is added as an option in the menu list"
+ );
+ let secondExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ name: "second_set_newtab",
+ browser_specific_settings: {
+ gecko: {
+ id: SECOND_ADDON_ID,
+ },
+ },
+ chrome_url_overrides: { newtab: "/newtab2.html" },
+ },
+ });
+ await secondExtension.startup();
+ await promise;
+ let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID);
+ is(
+ newTabMenuList.value,
+ SECOND_ADDON_ID,
+ "New tab menulist is set to the add-on"
+ );
+
+ levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
+ secondAddon.id,
+ NEW_TAB_KEY,
+ URL_OVERRIDES_TYPE
+ );
+ is(
+ levelOfControl,
+ "controlled_by_this_extension",
+ "getLevelOfControl returns controlled_by_this_extension."
+ );
+ setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY);
+ ok(
+ setting.value.startsWith("moz-extension") &&
+ setting.value.endsWith("newtab2.html"),
+ "The url_overrides is set by the second extension"
+ );
+
+ promise = TestUtils.waitForCondition(
+ () => newTabMenuList.itemCount == 3,
+ "addon option is no longer an option in the menu list after disable, even if it was not selected"
+ );
+ await addon.disable();
+ await promise;
+
+ // Ensure that re-enabling an addon adds it back to the menulist
+ promise = TestUtils.waitForCondition(
+ () => newTabMenuList.itemCount == 4,
+ "addon option is added again to the menulist when enabled"
+ );
+ await addon.enable();
+ await promise;
+
+ promise = TestUtils.waitForCondition(
+ () => newTabMenuList.itemCount == 3,
+ "addon option is no longer an option in the menu list after disable"
+ );
+ await secondAddon.disable();
+ await promise;
+
+ promise = TestUtils.waitForCondition(
+ () => newTabMenuList.itemCount == 4,
+ "addon option is added again to the menulist when enabled"
+ );
+ await secondAddon.enable();
+ await promise;
+
+ promise = TestUtils.waitForCondition(
+ () => newTabMenuList.itemCount == 2,
+ "addon options are all removed after disabling all"
+ );
+ await addon.disable();
+ await secondAddon.disable();
+ await promise;
+ is(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEWTAB,
+ "new tab page is set back to default"
+ );
+
+ // Cleanup the tabs and add-on.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await extension.unload();
+ await secondExtension.unload();
+});
+
+add_task(async function testExtensionControlledWebNotificationsPermission() {
+ let manifest = {
+ manifest_version: 2,
+ name: "TestExtension",
+ version: "1.0",
+ description: "Testing WebNotificationsDisable",
+ browser_specific_settings: { gecko: { id: "@web_notifications_disable" } },
+ permissions: ["browserSettings"],
+ browser_action: {
+ default_title: "Testing",
+ },
+ };
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await openNotificationsPermissionDialog();
+
+ let doc = sitePermissionsDialog.document;
+ let extensionControlledContent = doc.getElementById(
+ "browserNotificationsPermissionExtensionContent"
+ );
+
+ // Test that extension content is initially hidden.
+ ok(
+ extensionControlledContent.hidden,
+ "Extension content is initially hidden"
+ );
+
+ // Install an extension that will disable web notifications permission.
+ let messageShown = waitForMessageShown(
+ "browserNotificationsPermissionExtensionContent",
+ doc
+ );
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ useAddonManager: "permanent",
+ background() {
+ browser.browserSettings.webNotificationsDisabled.set({ value: true });
+ browser.test.sendMessage("load-extension");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("load-extension");
+ await messageShown;
+
+ let controlledDesc = extensionControlledContent.querySelector("description");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(controlledDesc),
+ {
+ id: "extension-controlling-web-notifications",
+ args: {
+ name: "TestExtension",
+ },
+ },
+ "The user is notified that an extension is controlling the web notifications permission"
+ );
+ is(
+ extensionControlledContent.hidden,
+ false,
+ "The extension controlled row is not hidden"
+ );
+
+ // Disable the extension.
+ doc.getElementById("disableNotificationsPermissionExtension").click();
+
+ // Verify the user is notified how to enable the extension.
+ await waitForEnableMessage(extensionControlledContent.id, doc);
+ is(
+ doc.l10n.getAttributes(controlledDesc.querySelector("label")).id,
+ "extension-controlled-enable",
+ "The user is notified of how to enable the extension again"
+ );
+
+ // Verify the enable message can be dismissed.
+ let hidden = waitForMessageHidden(extensionControlledContent.id, doc);
+ let dismissButton = controlledDesc.querySelector("image:last-of-type");
+ dismissButton.click();
+ await hidden;
+
+ // Verify that the extension controlled content in hidden again.
+ is(
+ extensionControlledContent.hidden,
+ true,
+ "The extension controlled row is now hidden"
+ );
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testExtensionControlledHomepageUninstalledAddon() {
+ async function checkHomepageEnabled() {
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#home",
+ "#home should be in the URI for about:preferences"
+ );
+
+ // The homepage is enabled.
+ let homepageInput = doc.getElementById("homePageUrl");
+ is(homepageInput.disabled, false, "The homepage input is enabled");
+ is(homepageInput.value, "", "The homepage input is empty");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ await ExtensionSettingsStore.initialize();
+
+ // Verify the setting isn't reported as controlled and the inputs are enabled.
+ is(
+ ExtensionSettingsStore.getSetting("prefs", "homepage_override"),
+ null,
+ "The homepage_override is not set"
+ );
+ await checkHomepageEnabled();
+
+ // Disarm any pending writes before we modify the JSONFile directly.
+ await ExtensionSettingsStore._reloadFile(false);
+
+ // Write out a bad store file.
+ let storeData = {
+ prefs: {
+ homepage_override: {
+ initialValue: "",
+ precedenceList: [
+ {
+ id: "bad@mochi.test",
+ installDate: 1508802672,
+ value: "https://developer.mozilla.org",
+ enabled: true,
+ },
+ ],
+ },
+ },
+ };
+ let jsonFileName = "extension-settings.json";
+ let storePath = PathUtils.join(PathUtils.profileDir, jsonFileName);
+
+ await IOUtils.writeUTF8(storePath, JSON.stringify(storeData));
+
+ // Reload the ExtensionSettingsStore so it will read the file on disk. Don't
+ // finalize the current store since it will overwrite our file.
+ await ExtensionSettingsStore._reloadFile(false);
+
+ // Verify that the setting is reported as set, but the homepage is still enabled
+ // since there is no matching installed extension.
+ is(
+ ExtensionSettingsStore.getSetting("prefs", "homepage_override").value,
+ "https://developer.mozilla.org",
+ "The homepage_override appears to be set"
+ );
+ await checkHomepageEnabled();
+
+ // Remove the bad store file that we used.
+ await IOUtils.remove(storePath);
+
+ // Reload the ExtensionSettingsStore again so it clears the data we added.
+ // Don't finalize the current store since it will write out the bad data.
+ await ExtensionSettingsStore._reloadFile(false);
+
+ is(
+ ExtensionSettingsStore.getSetting("prefs", "homepage_override"),
+ null,
+ "The ExtensionSettingsStore is left empty."
+ );
+});
+
+add_task(async function testExtensionControlledTrackingProtection() {
+ const TP_PREF = "privacy.trackingprotection.enabled";
+ const TP_DEFAULT = false;
+ const EXTENSION_ID = "@set_tp";
+ const CONTROLLED_LABEL_ID =
+ "contentBlockingTrackingProtectionExtensionContentLabel";
+ const CONTROLLED_BUTTON_ID =
+ "contentBlockingDisableTrackingProtectionExtension";
+
+ let tpEnabledPref = () => Services.prefs.getBoolPref(TP_PREF);
+
+ await SpecialPowers.pushPrefEnv({ set: [[TP_PREF, TP_DEFAULT]] });
+
+ function background() {
+ browser.privacy.websites.trackingProtectionMode.set({ value: "always" });
+ }
+
+ function verifyState(isControlled) {
+ is(tpEnabledPref(), isControlled, "TP pref is set to the expected value.");
+
+ let controlledLabel = doc.getElementById(CONTROLLED_LABEL_ID);
+ let controlledButton = doc.getElementById(CONTROLLED_BUTTON_ID);
+
+ is(
+ controlledLabel.hidden,
+ !isControlled,
+ "The extension controlled row's visibility is as expected."
+ );
+ is(
+ controlledButton.hidden,
+ !isControlled,
+ "The disable extension button's visibility is as expected."
+ );
+ if (isControlled) {
+ let controlledDesc = controlledLabel.querySelector("description");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(controlledDesc),
+ {
+ id: "extension-controlling-websites-content-blocking-all-trackers",
+ args: {
+ name: "set_tp",
+ },
+ },
+ "The user is notified that an extension is controlling TP."
+ );
+ }
+
+ is(
+ doc.getElementById("trackingProtectionMenu").disabled,
+ isControlled,
+ "TP control is enabled."
+ );
+ }
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#privacy",
+ "#privacy should be in the URI for about:preferences"
+ );
+
+ verifyState(false);
+
+ // Install an extension that sets Tracking Protection.
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ name: "set_tp",
+ browser_specific_settings: { gecko: { id: EXTENSION_ID } },
+ permissions: ["privacy"],
+ },
+ background,
+ });
+
+ let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID);
+ await extension.startup();
+ await messageShown;
+ let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+
+ verifyState(true);
+
+ await disableExtensionViaClick(
+ CONTROLLED_LABEL_ID,
+ CONTROLLED_BUTTON_ID,
+ doc
+ );
+
+ verifyState(false);
+
+ // Enable the extension so we get the UNINSTALL event, which is needed by
+ // ExtensionPreferencesManager to clean up properly.
+ // TODO: BUG 1408226
+ await reEnableExtension(addon, CONTROLLED_LABEL_ID);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testExtensionControlledPasswordManager() {
+ const PASSWORD_MANAGER_ENABLED_PREF = "signon.rememberSignons";
+ const PASSWORD_MANAGER_ENABLED_DEFAULT = true;
+ const CONTROLLED_BUTTON_ID = "disablePasswordManagerExtension";
+ const CONTROLLED_LABEL_ID = "passwordManagerExtensionContent";
+ const EXTENSION_ID = "@remember_signons";
+ let manifest = {
+ manifest_version: 2,
+ name: "testPasswordManagerExtension",
+ version: "1.0",
+ description: "Testing rememberSignons",
+ browser_specific_settings: { gecko: { id: EXTENSION_ID } },
+ permissions: ["privacy"],
+ browser_action: {
+ default_title: "Testing rememberSignons",
+ },
+ };
+
+ let passwordManagerEnabledPref = () =>
+ Services.prefs.getBoolPref(PASSWORD_MANAGER_ENABLED_PREF);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[PASSWORD_MANAGER_ENABLED_PREF, PASSWORD_MANAGER_ENABLED_DEFAULT]],
+ });
+ is(
+ passwordManagerEnabledPref(),
+ true,
+ "Password manager is enabled by default."
+ );
+
+ function verifyState(isControlled) {
+ is(
+ passwordManagerEnabledPref(),
+ !isControlled,
+ "Password manager pref is set to the expected value."
+ );
+ let controlledLabel =
+ gBrowser.contentDocument.getElementById(CONTROLLED_LABEL_ID);
+ let controlledButton =
+ gBrowser.contentDocument.getElementById(CONTROLLED_BUTTON_ID);
+ is(
+ controlledLabel.hidden,
+ !isControlled,
+ "The extension's controlled row visibility is as expected."
+ );
+ is(
+ controlledButton.hidden,
+ !isControlled,
+ "The extension's controlled button visibility is as expected."
+ );
+ if (isControlled) {
+ let controlledDesc = controlledLabel.querySelector("description");
+ Assert.deepEqual(
+ gBrowser.contentDocument.l10n.getAttributes(controlledDesc),
+ {
+ id: "extension-controlling-password-saving",
+ args: {
+ name: "testPasswordManagerExtension",
+ },
+ },
+ "The user is notified that an extension is controlling the remember signons pref."
+ );
+ }
+ }
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ info("Verify that no extension is controlling the password manager pref.");
+ verifyState(false);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ useAddonManager: "permanent",
+ background() {
+ browser.privacy.services.passwordSavingEnabled.set({ value: false });
+ },
+ });
+ let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID);
+ await extension.startup();
+ await messageShown;
+
+ info(
+ "Verify that the test extension is controlling the password manager pref."
+ );
+ verifyState(true);
+
+ info("Verify that the extension shows as controlled when loaded again.");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ verifyState(true);
+
+ await disableExtensionViaClick(
+ CONTROLLED_LABEL_ID,
+ CONTROLLED_BUTTON_ID,
+ gBrowser.contentDocument
+ );
+
+ info(
+ "Verify that disabling the test extension removes the lock on the password manager pref."
+ );
+ verifyState(false);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function testExtensionControlledProxyConfig() {
+ const proxySvc = Ci.nsIProtocolProxyService;
+ const PROXY_DEFAULT = proxySvc.PROXYCONFIG_SYSTEM;
+ const EXTENSION_ID = "@set_proxy";
+ const CONTROLLED_SECTION_ID = "proxyExtensionContent";
+ const CONTROLLED_BUTTON_ID = "disableProxyExtension";
+ const CONNECTION_SETTINGS_DESC_ID = "connectionSettingsDescription";
+ const PANEL_URL =
+ "chrome://browser/content/preferences/dialogs/connection.xhtml";
+
+ await SpecialPowers.pushPrefEnv({ set: [[PROXY_PREF, PROXY_DEFAULT]] });
+
+ function background() {
+ browser.proxy.settings.set({ value: { proxyType: "none" } });
+ }
+
+ function expectedConnectionSettingsMessage(doc, isControlled) {
+ return isControlled
+ ? "extension-controlling-proxy-config"
+ : "network-proxy-connection-description";
+ }
+
+ function connectionSettingsMessagePromise(doc, isControlled) {
+ return waitForMessageContent(
+ CONNECTION_SETTINGS_DESC_ID,
+ expectedConnectionSettingsMessage(doc, isControlled),
+ doc
+ );
+ }
+
+ function verifyProxyState(doc, isControlled) {
+ let isPanel = doc.getElementById(CONTROLLED_BUTTON_ID);
+ is(
+ proxyType === proxySvc.PROXYCONFIG_DIRECT,
+ isControlled,
+ "Proxy pref is set to the expected value."
+ );
+
+ if (isPanel) {
+ let controlledSection = doc.getElementById(CONTROLLED_SECTION_ID);
+
+ is(
+ controlledSection.hidden,
+ !isControlled,
+ "The extension controlled row's visibility is as expected."
+ );
+ if (isPanel) {
+ is(
+ doc.getElementById(CONTROLLED_BUTTON_ID).hidden,
+ !isControlled,
+ "The disable extension button's visibility is as expected."
+ );
+ }
+ if (isControlled) {
+ let controlledDesc = controlledSection.querySelector("description");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(controlledDesc),
+ {
+ id: "extension-controlling-proxy-config",
+ args: {
+ name: "set_proxy",
+ },
+ },
+ "The user is notified that an extension is controlling proxy settings."
+ );
+ }
+ function getProxyControls() {
+ let controlGroup = doc.getElementById("networkProxyType");
+ let manualControlContainer = controlGroup.querySelector("#proxy-grid");
+ return {
+ manualControls: [
+ ...manualControlContainer.querySelectorAll(
+ "label[data-l10n-id]:not([control=networkProxyNone])"
+ ),
+ ...manualControlContainer.querySelectorAll("input"),
+ ...manualControlContainer.querySelectorAll("checkbox"),
+ ...doc.querySelectorAll("#networkProxySOCKSVersion > radio"),
+ ],
+ pacControls: [doc.getElementById("networkProxyAutoconfigURL")],
+ otherControls: [
+ doc.querySelector("label[control=networkProxyNone]"),
+ doc.getElementById("networkProxyNone"),
+ ...controlGroup.querySelectorAll(":scope > radio"),
+ ...doc.querySelectorAll("#ConnectionsDialogPane > checkbox"),
+ ],
+ };
+ }
+ let controlState = isControlled ? "disabled" : "enabled";
+ let controls = getProxyControls();
+ for (let element of controls.manualControls) {
+ let disabled =
+ isControlled || proxyType !== proxySvc.PROXYCONFIG_MANUAL;
+ is(
+ element.disabled,
+ disabled,
+ `Manual proxy controls should be ${controlState} - control: ${element.outerHTML}.`
+ );
+ }
+ for (let element of controls.pacControls) {
+ let disabled = isControlled || proxyType !== proxySvc.PROXYCONFIG_PAC;
+ is(
+ element.disabled,
+ disabled,
+ `PAC proxy controls should be ${controlState} - control: ${element.outerHTML}.`
+ );
+ }
+ for (let element of controls.otherControls) {
+ is(
+ element.disabled,
+ isControlled,
+ `Other proxy controls should be ${controlState} - control: ${element.outerHTML}.`
+ );
+ }
+ } else {
+ let elem = doc.getElementById(CONNECTION_SETTINGS_DESC_ID);
+ is(
+ doc.l10n.getAttributes(elem).id,
+ expectedConnectionSettingsMessage(doc, isControlled),
+ "The connection settings description is as expected."
+ );
+ }
+ }
+
+ async function reEnableProxyExtension(addon) {
+ let messageChanged = connectionSettingsMessagePromise(mainDoc, true);
+ await addon.enable();
+ await messageChanged;
+ }
+
+ async function openProxyPanel() {
+ let panel = await openAndLoadSubDialog(PANEL_URL);
+ let closingPromise = BrowserTestUtils.waitForEvent(
+ panel.document.getElementById("ConnectionsDialog"),
+ "dialogclosing"
+ );
+ ok(panel, "Proxy panel opened.");
+ return { panel, closingPromise };
+ }
+
+ async function closeProxyPanel(panelObj) {
+ let dialog = panelObj.panel.document.getElementById("ConnectionsDialog");
+ dialog.cancelDialog();
+ let panelClosingEvent = await panelObj.closingPromise;
+ ok(panelClosingEvent, "Proxy panel closed.");
+ }
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let mainDoc = gBrowser.contentDocument;
+
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#general",
+ "#general should be in the URI for about:preferences"
+ );
+
+ verifyProxyState(mainDoc, false);
+
+ // Open the connections panel.
+ let panelObj = await openProxyPanel();
+ let panelDoc = panelObj.panel.document;
+
+ verifyProxyState(panelDoc, false);
+
+ await closeProxyPanel(panelObj);
+
+ verifyProxyState(mainDoc, false);
+
+ // Install an extension that controls proxy settings. The extension needs
+ // incognitoOverride because controlling the proxy.settings requires private
+ // browsing access.
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ useAddonManager: "permanent",
+ manifest: {
+ name: "set_proxy",
+ browser_specific_settings: { gecko: { id: EXTENSION_ID } },
+ permissions: ["proxy"],
+ },
+ background,
+ });
+
+ let messageChanged = connectionSettingsMessagePromise(mainDoc, true);
+ await extension.startup();
+ await messageChanged;
+ let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+
+ verifyProxyState(mainDoc, true);
+ messageChanged = connectionSettingsMessagePromise(mainDoc, false);
+
+ panelObj = await openProxyPanel();
+ panelDoc = panelObj.panel.document;
+
+ verifyProxyState(panelDoc, true);
+
+ await disableExtensionViaClick(
+ CONTROLLED_SECTION_ID,
+ CONTROLLED_BUTTON_ID,
+ panelDoc
+ );
+
+ verifyProxyState(panelDoc, false);
+
+ await closeProxyPanel(panelObj);
+ await messageChanged;
+
+ verifyProxyState(mainDoc, false);
+
+ await reEnableProxyExtension(addon);
+
+ verifyProxyState(mainDoc, true);
+ messageChanged = connectionSettingsMessagePromise(mainDoc, false);
+
+ panelObj = await openProxyPanel();
+ panelDoc = panelObj.panel.document;
+
+ verifyProxyState(panelDoc, true);
+
+ await disableExtensionViaClick(
+ CONTROLLED_SECTION_ID,
+ CONTROLLED_BUTTON_ID,
+ panelDoc
+ );
+
+ verifyProxyState(panelDoc, false);
+
+ await closeProxyPanel(panelObj);
+ await messageChanged;
+
+ verifyProxyState(mainDoc, false);
+
+ // Enable the extension so we get the UNINSTALL event, which is needed by
+ // ExtensionPreferencesManager to clean up properly.
+ // TODO: BUG 1408226
+ await reEnableProxyExtension(addon);
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test that the newtab menu selection is correct when loading about:preferences
+add_task(async function testMenuSyncFromPrefs() {
+ const DEFAULT_NEWTAB = "about:newtab";
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#home",
+ "#home should be in the URI for about:preferences"
+ );
+
+ let doc = gBrowser.contentDocument;
+ let newTabMenuList = doc.getElementById("newTabMode");
+ // The new tab page is set to the default.
+ is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default");
+
+ is(newTabMenuList.value, "0", "New tab menulist is set to the default");
+
+ newTabMenuList.value = "1";
+ newTabMenuList.dispatchEvent(new Event("command"));
+ is(newTabMenuList.value, "1", "New tab menulist is set to blank");
+
+ gBrowser.reloadTab(gBrowser.selectedTab);
+
+ await TestUtils.waitForCondition(
+ () => gBrowser.contentDocument.getElementById("newTabMode"),
+ "wait until element exists in new contentDoc"
+ );
+
+ is(
+ gBrowser.contentDocument.getElementById("newTabMode").value,
+ "1",
+ "New tab menulist is still set to blank"
+ );
+
+ // Cleanup
+ newTabMenuList.value = "0";
+ newTabMenuList.dispatchEvent(new Event("command"));
+ is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_filetype_dialog.js b/browser/components/preferences/tests/browser_filetype_dialog.js
new file mode 100644
index 0000000000..0e5ac036c4
--- /dev/null
+++ b/browser/components/preferences/tests/browser_filetype_dialog.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+SimpleTest.requestCompleteLog();
+const { HandlerServiceTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/HandlerServiceTestUtils.sys.mjs"
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+let gOldMailHandlers = [];
+let gDummyHandlers = [];
+let gOriginalPreferredMailHandler;
+let gOriginalPreferredPDFHandler;
+
+registerCleanupFunction(function () {
+ function removeDummyHandlers(handlers) {
+ // Remove any of the dummy handlers we created.
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ if (
+ gDummyHandlers.some(
+ h =>
+ h.uriTemplate ==
+ handlers.queryElementAt(i, Ci.nsIWebHandlerApp).uriTemplate
+ )
+ ) {
+ handlers.removeElementAt(i);
+ }
+ } catch (ex) {
+ /* ignore non-web-app handlers */
+ }
+ }
+ }
+ // Re-add the original protocol handlers:
+ let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
+ let mailHandlers = mailHandlerInfo.possibleApplicationHandlers;
+ for (let h of gOldMailHandlers) {
+ mailHandlers.appendElement(h);
+ }
+ removeDummyHandlers(mailHandlers);
+ mailHandlerInfo.preferredApplicationHandler = gOriginalPreferredMailHandler;
+ gHandlerService.store(mailHandlerInfo);
+
+ let pdfHandlerInfo =
+ HandlerServiceTestUtils.getHandlerInfo("application/pdf");
+ pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally;
+ pdfHandlerInfo.preferredApplicationHandler = gOriginalPreferredPDFHandler;
+ let handlers = pdfHandlerInfo.possibleApplicationHandlers;
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
+ if (app.name == "Foopydoopydoo") {
+ handlers.removeElementAt(i);
+ }
+ }
+ gHandlerService.store(pdfHandlerInfo);
+
+ gBrowser.removeCurrentTab();
+});
+
+function scrubMailtoHandlers(handlerInfo) {
+ // Remove extant web handlers because they have icons that
+ // we fetch from the web, which isn't allowed in tests.
+ let handlers = handlerInfo.possibleApplicationHandlers;
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ gOldMailHandlers.push(handler);
+ // If we get here, this is a web handler app. Remove it:
+ handlers.removeElementAt(i);
+ } catch (ex) {}
+ }
+}
+
+("use strict");
+
+add_setup(async function () {
+ // Create our dummy handlers
+ let handler1 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ handler1.name = "Handler 1";
+ handler1.uriTemplate = "https://example.com/first/%s";
+
+ let handler2 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ handler2.name = "Handler 2";
+ handler2.uriTemplate = "http://example.org/second/%s";
+ gDummyHandlers.push(handler1, handler2);
+
+ function substituteWebHandlers(handlerInfo) {
+ // Append the dummy handlers to replace them:
+ let handlers = handlerInfo.possibleApplicationHandlers;
+ handlers.appendElement(handler1);
+ handlers.appendElement(handler2);
+ gHandlerService.store(handlerInfo);
+ }
+ // Set up our mailto handler test infrastructure.
+ let mailtoHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
+ scrubMailtoHandlers(mailtoHandlerInfo);
+ gOriginalPreferredMailHandler = mailtoHandlerInfo.preferredApplicationHandler;
+ substituteWebHandlers(mailtoHandlerInfo);
+
+ // Now add a pdf handler:
+ let pdfHandlerInfo =
+ HandlerServiceTestUtils.getHandlerInfo("application/pdf");
+ // PDF doesn't have built-in web handlers, so no need to scrub.
+ gOriginalPreferredPDFHandler = pdfHandlerInfo.preferredApplicationHandler;
+ let handlers = pdfHandlerInfo.possibleApplicationHandlers;
+ let appHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ appHandler.name = "Foopydoopydoo";
+ appHandler.executable = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ appHandler.executable.append("dummy.exe");
+ // Prefs are picky and want this to exist and be executable (bug 1626009):
+ if (!appHandler.executable.exists()) {
+ appHandler.executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o777);
+ }
+
+ handlers.appendElement(appHandler);
+
+ pdfHandlerInfo.preferredApplicationHandler = appHandler;
+ pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ gHandlerService.store(pdfHandlerInfo);
+
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ info("Preferences page opened on the general pane.");
+
+ await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList;
+ info("Apps list loaded.");
+});
+
+add_task(async function dialogShowsCorrectContent() {
+ let win = gBrowser.selectedBrowser.contentWindow;
+
+ let container = win.document.getElementById("handlersView");
+
+ // First, find the PDF item.
+ let pdfItem = container.querySelector("richlistitem[type='application/pdf']");
+ Assert.ok(pdfItem, "pdfItem is present in handlersView.");
+ pdfItem.scrollIntoView({ block: "center" });
+ pdfItem.closest("richlistbox").selectItem(pdfItem);
+
+ // Open its menu
+ let list = pdfItem.querySelector(".actionsMenu");
+ let popup = list.menupopup;
+ let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(list, {}, win);
+ await popupShown;
+
+ // Then open the dialog
+ const promiseDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/applicationManager.xhtml"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ popup.querySelector(".manage-app-item"),
+ {},
+ win
+ );
+ let dialogWin = await promiseDialogLoaded;
+
+ // Then verify that the description is correct.
+ let desc = dialogWin.document.getElementById("appDescription");
+ let descL10n = dialogWin.document.l10n.getAttributes(desc);
+ is(descL10n.id, "app-manager-handle-file", "Should have right string");
+ let stringBundle = Services.strings.createBundle(
+ "chrome://mozapps/locale/downloads/unknownContentType.properties"
+ );
+ is(
+ descL10n.args.type,
+ stringBundle.GetStringFromName("pdfExtHandlerDescription"),
+ "Should have PDF string bits."
+ );
+
+ // And that there's one item in the list, with the correct name:
+ let appList = dialogWin.document.getElementById("appList");
+ is(appList.itemCount, 1, "Should have 1 item in the list");
+ is(
+ appList.selectedItem.querySelector("label").getAttribute("value"),
+ "Foopydoopydoo",
+ "Should have the right executable label"
+ );
+
+ dialogWin.close();
+});
diff --git a/browser/components/preferences/tests/browser_fluent.js b/browser/components/preferences/tests/browser_fluent.js
new file mode 100644
index 0000000000..db2daecc4f
--- /dev/null
+++ b/browser/components/preferences/tests/browser_fluent.js
@@ -0,0 +1,40 @@
+function whenMainPaneLoadedFinished() {
+ return new Promise(function (resolve, reject) {
+ const topic = "main-pane-loaded";
+ Services.obs.addObserver(function observer(aSubject) {
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }, topic);
+ });
+}
+
+// Temporary test for an experimental new localization API.
+// See bug 1402069 for details.
+add_task(async function () {
+ // The string is used only when `browserTabsRemoteAutostart` is true
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ ok(true, "fake test to avoid harness complaining");
+ return;
+ }
+
+ await Promise.all([
+ openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }),
+ whenMainPaneLoadedFinished(),
+ ]);
+
+ let doc = gBrowser.contentDocument;
+ await doc.l10n.ready;
+
+ let [msg] = await doc.l10n.formatMessages([{ id: "category-general" }]);
+
+ let elem = doc.querySelector(`#category-general`);
+
+ Assert.deepEqual(msg, {
+ value: null,
+ attributes: [
+ { name: "tooltiptext", value: elem.getAttribute("tooltiptext") },
+ ],
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_homepage_default.js b/browser/components/preferences/tests/browser_homepage_default.js
new file mode 100644
index 0000000000..e3fcdcec5e
--- /dev/null
+++ b/browser/components/preferences/tests/browser_homepage_default.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function default_homepage_test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.startup.page", 1]],
+ });
+ let defaults = Services.prefs.getDefaultBranch("");
+ // Simulate a homepage set via policy or a distribution.
+ defaults.setStringPref("browser.startup.homepage", "https://example.com");
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+
+ let doc = gBrowser.contentDocument;
+ let homeMode = doc.getElementById("homeMode");
+ Assert.equal(homeMode.value, 2, "homeMode should be 2 (Custom URL)");
+
+ let homePageUrl = doc.getElementById("homePageUrl");
+ Assert.equal(
+ homePageUrl.value,
+ "https://example.com",
+ "homePageUrl should be example.com"
+ );
+
+ registerCleanupFunction(async () => {
+ defaults.setStringPref("browser.startup.homepage", "about:home");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
diff --git a/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js b/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js
new file mode 100644
index 0000000000..f54d0edaaa
--- /dev/null
+++ b/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js
@@ -0,0 +1,33 @@
+add_task(async function testSetHomepageUseCurrent() {
+ is(
+ gBrowser.currentURI.spec,
+ "about:blank",
+ "Test starts with about:blank open"
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+ let doc = gBrowser.contentDocument;
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences#home",
+ "#home should be in the URI for about:preferences"
+ );
+ let oldHomepage = HomePage.get();
+
+ let useCurrent = doc.getElementById("useCurrentBtn");
+ useCurrent.click();
+
+ is(gBrowser.tabs.length, 3, "Three tabs should be open");
+ await TestUtils.waitForCondition(
+ () => HomePage.get() == "about:blank|about:home"
+ );
+ is(
+ HomePage.get(),
+ "about:blank|about:home",
+ "about:blank and about:home should be the only homepages set"
+ );
+
+ HomePage.safeSet(oldHomepage);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_homepages_use_bookmark.js b/browser/components/preferences/tests/browser_homepages_use_bookmark.js
new file mode 100644
index 0000000000..572783481d
--- /dev/null
+++ b/browser/components/preferences/tests/browser_homepages_use_bookmark.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL1 = "http://example.com/1";
+const TEST_URL2 = "http://example.com/2";
+
+add_setup(async function () {
+ let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage");
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "about:preferences#home",
+ "#home should be in the URI for about:preferences"
+ );
+
+ registerCleanupFunction(async () => {
+ Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function testSetHomepageFromBookmark() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "TestHomepage",
+ url: TEST_URL1,
+ });
+
+ let doc = gBrowser.contentDocument;
+ // Select the custom URLs option.
+ doc.getElementById("homeMode").value = 2;
+
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml"
+ );
+ doc.getElementById("useBookmarkBtn").click();
+
+ let dialog = await promiseSubDialogLoaded;
+ dialog.document.getElementById("bookmarks").selectItems([bm.guid]);
+ dialog.document
+ .getElementById("selectBookmarkDialog")
+ .getButton("accept")
+ .click();
+
+ await TestUtils.waitForCondition(() => HomePage.get() == TEST_URL1);
+
+ Assert.equal(
+ HomePage.get(),
+ TEST_URL1,
+ "Should have set the homepage to the same as the bookmark."
+ );
+});
+
+add_task(async function testSetHomepageFromTopLevelFolder() {
+ // Insert a second item into the menu folder
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "TestHomepage",
+ url: TEST_URL2,
+ });
+
+ let doc = gBrowser.contentDocument;
+ // Select the custom URLs option.
+ doc.getElementById("homeMode").value = 2;
+
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml"
+ );
+ doc.getElementById("useBookmarkBtn").click();
+
+ let dialog = await promiseSubDialogLoaded;
+ dialog.document
+ .getElementById("bookmarks")
+ .selectItems([PlacesUtils.bookmarks.menuGuid]);
+ dialog.document
+ .getElementById("selectBookmarkDialog")
+ .getButton("accept")
+ .click();
+
+ await TestUtils.waitForCondition(
+ () => HomePage.get() == `${TEST_URL1}|${TEST_URL2}`
+ );
+
+ Assert.equal(
+ HomePage.get(),
+ `${TEST_URL1}|${TEST_URL2}`,
+ "Should have set the homepage to the same as the bookmark."
+ );
+});
diff --git a/browser/components/preferences/tests/browser_hometab_restore_defaults.js b/browser/components/preferences/tests/browser_hometab_restore_defaults.js
new file mode 100644
index 0000000000..55a9a974b2
--- /dev/null
+++ b/browser/components/preferences/tests/browser_hometab_restore_defaults.js
@@ -0,0 +1,220 @@
+add_task(async function testRestoreDefaultsBtn_visible() {
+ const before = SpecialPowers.Services.prefs.getStringPref(
+ "browser.newtabpage.activity-stream.feeds.section.topstories.options",
+ ""
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Hide Pocket prefs so we don't trigger network requests when we reset all preferences
+ [
+ "browser.newtabpage.activity-stream.feeds.section.topstories.options",
+ JSON.stringify(Object.assign({}, JSON.parse(before), { hidden: true })),
+ ],
+ [
+ "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear",
+ "",
+ ],
+ // Set a user pref to false to force the Restore Defaults button to be visible
+ ["browser.newtabpage.activity-stream.feeds.topsites", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#home",
+ false
+ );
+ let browser = tab.linkedBrowser;
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.getElementById("restoreDefaultHomePageBtn") !== null
+ ),
+ "Wait for the button to be added to the page"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.querySelector(
+ "[data-subcategory='topsites'] checkbox"
+ ) !== null
+ ),
+ "Wait for the preference checkbox to load"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.getElementById("restoreDefaultHomePageBtn")
+ .hidden === false
+ ),
+ "Should show the Restore Defaults btn because pref is changed"
+ );
+
+ await SpecialPowers.spawn(browser, [], () =>
+ content.document.getElementById("restoreDefaultHomePageBtn").click()
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.querySelector(
+ "[data-subcategory='topsites'] checkbox"
+ ).checked
+ ),
+ "Should have checked preference"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.getElementById("restoreDefaultHomePageBtn").style
+ .visibility === "hidden"
+ ),
+ "Should not show the Restore Defaults btn if prefs were reset"
+ );
+
+ const topsitesPref = await SpecialPowers.Services.prefs.getBoolPref(
+ "browser.newtabpage.activity-stream.feeds.topsites"
+ );
+ Assert.ok(topsitesPref, "Topsites pref should have the default value");
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testRestoreDefaultsBtn_hidden() {
+ const before = SpecialPowers.Services.prefs.getStringPref(
+ "browser.newtabpage.activity-stream.feeds.section.topstories.options",
+ ""
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Hide Pocket pref so we don't trigger network requests when we reset all preferences
+ [
+ "browser.newtabpage.activity-stream.feeds.section.topstories.options",
+ JSON.stringify(Object.assign({}, JSON.parse(before), { hidden: true })),
+ ],
+ [
+ "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear",
+ "",
+ ],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#home",
+ false
+ );
+ let browser = tab.linkedBrowser;
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.getElementById("restoreDefaultHomePageBtn") !== null
+ ),
+ "Wait for the button to be added to the page"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.querySelector(
+ "[data-subcategory='topsites'] checkbox"
+ ) !== null
+ ),
+ "Wait for the preference checkbox to load"
+ );
+
+ const btnDefault = await SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.getElementById("restoreDefaultHomePageBtn").style
+ .visibility
+ );
+ Assert.equal(
+ btnDefault,
+ "hidden",
+ "When no prefs are changed button should not show up"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.querySelector(
+ "[data-subcategory='topsites'] checkbox"
+ ).checked
+ ),
+ "Should have checked preference"
+ );
+
+ // Uncheck a pref
+ await SpecialPowers.spawn(browser, [], () =>
+ content.document
+ .querySelector("[data-subcategory='topsites'] checkbox")
+ .click()
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ !content.document.querySelector(
+ "[data-subcategory='topsites'] checkbox"
+ ).checked
+ ),
+ "Should have unchecked preference"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ SpecialPowers.spawn(
+ browser,
+ [],
+ () =>
+ content.document.getElementById("restoreDefaultHomePageBtn").style
+ .visibility === ""
+ ),
+ "Should show the Restore Defaults btn if prefs were changed"
+ );
+
+ // Reset the pref
+ await SpecialPowers.Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.feeds.topsites"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/preferences/tests/browser_https_only_exceptions.js b/browser/components/preferences/tests/browser_https_only_exceptions.js
new file mode 100644
index 0000000000..ad7cd34571
--- /dev/null
+++ b/browser/components/preferences/tests/browser_https_only_exceptions.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * First Test
+ * Checks if buttons are disabled/enabled and visible/hidden correctly.
+ */
+add_task(async function testButtons() {
+ // Let's make sure HTTPS-Only Mode is off.
+ await setHttpsOnlyPref("off");
+
+ // Open the privacy-pane in about:preferences
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ // Get button-element to open the exceptions-dialog
+ const exceptionButton = gBrowser.contentDocument.getElementById(
+ "httpsOnlyExceptionButton"
+ );
+
+ is(
+ exceptionButton.disabled,
+ true,
+ "HTTPS-Only exception button should be disabled when HTTPS-Only Mode is disabled."
+ );
+
+ await setHttpsOnlyPref("private");
+ is(
+ exceptionButton.disabled,
+ true,
+ "HTTPS-Only exception button should be disabled when HTTPS-Only Mode is only enabled in private browsing."
+ );
+
+ await setHttpsOnlyPref("everywhere");
+ is(
+ exceptionButton.disabled,
+ false,
+ "HTTPS-Only exception button should be enabled when HTTPS-Only Mode enabled everywhere."
+ );
+
+ // Now that the button is clickable, we open the dialog
+ // to check if the correct buttons are visible
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml"
+ );
+ exceptionButton.doCommand();
+
+ let win = await promiseSubDialogLoaded;
+
+ const dialogDoc = win.document;
+
+ is(
+ dialogDoc.getElementById("btnBlock").hidden,
+ true,
+ "Block button should not be visible in HTTPS-Only Dialog."
+ );
+ is(
+ dialogDoc.getElementById("btnCookieSession").hidden,
+ true,
+ "Cookie specific allow button should not be visible in HTTPS-Only Dialog."
+ );
+ is(
+ dialogDoc.getElementById("btnAllow").hidden,
+ true,
+ "Allow button should not be visible in HTTPS-Only Dialog."
+ );
+ is(
+ dialogDoc.getElementById("btnHttpsOnlyOff").hidden,
+ false,
+ "HTTPS-Only off button should be visible in HTTPS-Only Dialog."
+ );
+ is(
+ dialogDoc.getElementById("btnHttpsOnlyOffTmp").hidden,
+ false,
+ "HTTPS-Only temporary off button should be visible in HTTPS-Only Dialog."
+ );
+
+ // Reset prefs and close the tab
+ await SpecialPowers.flushPrefEnv();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Second Test
+ * Checks permissions are added and removed correctly.
+ *
+ * Each test opens a new dialog, performs an action (second argument),
+ * then closes the dialog and checks if the changes were made (third argument).
+ */
+add_task(async function checkDialogFunctionality() {
+ // Enable HTTPS-Only Mode for every window, so the exceptions dialog is accessible.
+ await setHttpsOnlyPref("everywhere");
+
+ // Open the privacy-pane in about:preferences
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ const preferencesDoc = gBrowser.contentDocument;
+
+ // Test if we can add permanent exceptions
+ await runTest(
+ preferencesDoc,
+ elements => {
+ assertListContents(elements, []);
+
+ elements.url.value = "test.com";
+ elements.btnAllow.doCommand();
+
+ assertListContents(elements, [
+ ["http://test.com", elements.allowL10nId],
+ ["https://test.com", elements.allowL10nId],
+ ]);
+ },
+ () => [
+ {
+ type: "https-only-load-insecure",
+ origin: "http://test.com",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ expireType: Ci.nsIPermissionManager.EXPIRE_NEVER,
+ },
+ {
+ type: "https-only-load-insecure",
+ origin: "https://test.com",
+ data: "added",
+ capability: Ci.nsIPermissionManager.ALLOW_ACTION,
+ expireType: Ci.nsIPermissionManager.EXPIRE_NEVER,
+ },
+ ]
+ );
+
+ // Test if items are retained, and if temporary exceptions are added correctly
+ await runTest(
+ preferencesDoc,
+ elements => {
+ assertListContents(elements, [
+ ["http://test.com", elements.allowL10nId],
+ ["https://test.com", elements.allowL10nId],
+ ]);
+
+ elements.url.value = "1.1.1.1:8080";
+ elements.btnAllowSession.doCommand();
+
+ assertListContents(elements, [
+ ["http://test.com", elements.allowL10nId],
+ ["https://test.com", elements.allowL10nId],
+ ["http://1.1.1.1:8080", elements.allowSessionL10nId],
+ ["https://1.1.1.1:8080", elements.allowSessionL10nId],
+ ]);
+ },
+ () => [
+ {
+ type: "https-only-load-insecure",
+ origin: "http://1.1.1.1:8080",
+ data: "added",
+ capability: Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
+ expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
+ },
+ {
+ type: "https-only-load-insecure",
+ origin: "https://1.1.1.1:8080",
+ data: "added",
+ capability: Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
+ expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
+ },
+ ]
+ );
+
+ await runTest(
+ preferencesDoc,
+ elements => {
+ while (elements.richlistbox.itemCount) {
+ elements.richlistbox.selectedIndex = 0;
+ elements.btnRemove.doCommand();
+ }
+ assertListContents(elements, []);
+ },
+ elements => {
+ let richlistItems = elements.richlistbox.getElementsByAttribute(
+ "origin",
+ "*"
+ );
+ let observances = [];
+ for (let item of richlistItems) {
+ observances.push({
+ type: "https-only-load-insecure",
+ origin: item.getAttribute("origin"),
+ data: "deleted",
+ });
+ }
+ return observances;
+ }
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Changes HTTPS-Only Mode pref
+ * @param {string} state "everywhere", "private", "off"
+ */
+async function setHttpsOnlyPref(state) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_only_mode", state === "everywhere"],
+ ["dom.security.https_only_mode_pbm", state === "private"],
+ ],
+ });
+}
+
+/**
+ * Opens new exceptions dialog, runs test function
+ * @param {HTMLElement} preferencesDoc document of about:preferences tab
+ * @param {function} test function to call when dialog is open
+ * @param {Array} observances permission changes to observe (order is important)
+ */
+async function runTest(preferencesDoc, test, observancesFn) {
+ // Click on exception-button and wait for dialog to open
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml"
+ );
+ preferencesDoc.getElementById("httpsOnlyExceptionButton").doCommand();
+
+ let win = await promiseSubDialogLoaded;
+
+ // Create a bunch of references to UI-elements for the test-function
+ const doc = win.document;
+ let elements = {
+ richlistbox: doc.getElementById("permissionsBox"),
+ url: doc.getElementById("url"),
+ btnAllow: doc.getElementById("btnHttpsOnlyOff"),
+ btnAllowSession: doc.getElementById("btnHttpsOnlyOffTmp"),
+ btnRemove: doc.getElementById("removePermission"),
+ allowL10nId: win.gPermissionManager._getCapabilityL10nId(
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ ),
+ allowSessionL10nId: win.gPermissionManager._getCapabilityL10nId(
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
+ ),
+ };
+
+ // Some observances need to be computed based on the current state.
+ const observances = observancesFn(elements);
+
+ // Run test function
+ await test(elements);
+
+ // Click on "Save changes" and wait for permission changes.
+ let btnApplyChanges = doc.querySelector("dialog").getButton("accept");
+ let observeAllPromise = createObserveAllPromise(observances);
+
+ btnApplyChanges.doCommand();
+ await observeAllPromise;
+}
+
+function assertListContents(elements, expected) {
+ is(
+ elements.richlistbox.itemCount,
+ expected.length,
+ "Richlistbox should match the expected amount of exceptions."
+ );
+
+ for (let i = 0; i < expected.length; i++) {
+ let website = expected[i][0];
+ let listItem = elements.richlistbox.getElementsByAttribute(
+ "origin",
+ website
+ );
+ is(listItem.length, 1, "Each origin should be unique");
+ is(
+ listItem[0]
+ .querySelector(".website-capability-value")
+ .getAttribute("data-l10n-id"),
+ expected[i][1],
+ "List item capability should match expected l10n-id"
+ );
+ }
+}
diff --git a/browser/components/preferences/tests/browser_https_only_section.js b/browser/components/preferences/tests/browser_https_only_section.js
new file mode 100644
index 0000000000..a43448adf5
--- /dev/null
+++ b/browser/components/preferences/tests/browser_https_only_section.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 1671122 - Fixed bug where second click on HTTPS-Only Mode enable-checkbox disables it again.
+// https://bugzilla.mozilla.org/bug/1671122
+"use strict";
+
+const HTTPS_ONLY_ENABLED = "enabled";
+const HTTPS_ONLY_PBM_ONLY = "privateOnly";
+const HTTPS_ONLY_DISABLED = "disabled";
+
+add_task(async function httpsOnlyRadioGroupIsWorking() {
+ // Make sure HTTPS-Only mode is only enabled for PBM
+
+ registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref("dom.security.https_only_mode");
+ Services.prefs.clearUserPref("dom.security.https_only_mode_pbm");
+ });
+
+ await SpecialPowers.setBoolPref("dom.security.https_only_mode", false);
+ await SpecialPowers.setBoolPref("dom.security.https_only_mode_pbm", true);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const radioGroup = doc.getElementById("httpsOnlyRadioGroup");
+ const enableAllRadio = doc.getElementById("httpsOnlyRadioEnabled");
+ const enablePbmRadio = doc.getElementById("httpsOnlyRadioEnabledPBM");
+ const disableRadio = doc.getElementById("httpsOnlyRadioDisabled");
+
+ // Check if UI
+ check(radioGroup, HTTPS_ONLY_PBM_ONLY);
+
+ // Check if UI updated on pref-change
+ await SpecialPowers.setBoolPref("dom.security.https_only_mode_pbm", false);
+ check(radioGroup, HTTPS_ONLY_DISABLED);
+
+ // Check if prefs change if clicked on radio button
+ enableAllRadio.click();
+ check(radioGroup, HTTPS_ONLY_ENABLED);
+
+ // Check if prefs stay the same if clicked on same
+ // radio button again (see bug 1671122)
+ enableAllRadio.click();
+ check(radioGroup, HTTPS_ONLY_ENABLED);
+
+ // Check if prefs are set correctly for PBM-only mode.
+ enablePbmRadio.click();
+ check(radioGroup, HTTPS_ONLY_PBM_ONLY);
+
+ // Check if prefs are set correctly when disabled again.
+ disableRadio.click();
+ check(radioGroup, HTTPS_ONLY_DISABLED);
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+function check(radioGroupElement, expectedValue) {
+ is(
+ radioGroupElement.value,
+ expectedValue,
+ "Radio Group value should match expected value"
+ );
+ is(
+ SpecialPowers.getBoolPref("dom.security.https_only_mode"),
+ expectedValue === HTTPS_ONLY_ENABLED,
+ "HTTPS-Only pref should match expected value."
+ );
+ is(
+ SpecialPowers.getBoolPref("dom.security.https_only_mode_pbm"),
+ expectedValue === HTTPS_ONLY_PBM_ONLY,
+ "HTTPS-Only PBM pref should match expected value."
+ );
+}
diff --git a/browser/components/preferences/tests/browser_ignore_invalid_capability.js b/browser/components/preferences/tests/browser_ignore_invalid_capability.js
new file mode 100644
index 0000000000..35b2679ffc
--- /dev/null
+++ b/browser/components/preferences/tests/browser_ignore_invalid_capability.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function testInvalidCapabilityIgnored() {
+ info(
+ "Test to make sure that invalid combinations of type and capability are ignored \
+ so the cookieExceptions management popup does not crash"
+ );
+ PermissionTestUtils.add(
+ "https://mozilla.org",
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ // This is an invalid combination of type & capability and should be ignored
+ PermissionTestUtils.add(
+ "https://foobar.org",
+ "cookie",
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml"
+ );
+ doc.getElementById("cookieExceptions").doCommand();
+
+ let win = await promiseSubDialogLoaded;
+ doc = win.document;
+
+ is(
+ doc.getElementById("permissionsBox").itemCount,
+ 1,
+ "We only display the permission that is valid for the type cookie"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_languages_subdialog.js b/browser/components/preferences/tests/browser_languages_subdialog.js
new file mode 100644
index 0000000000..e85ce44ca3
--- /dev/null
+++ b/browser/components/preferences/tests/browser_languages_subdialog.js
@@ -0,0 +1,139 @@
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const contentDocument = gBrowser.contentDocument;
+ let dialogOverlay = content.gSubDialog._preloadDialog._overlay;
+
+ async function languagesSubdialogOpened() {
+ const promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/languages.xhtml"
+ );
+ contentDocument.getElementById("chooseLanguage").click();
+ const win = await promiseSubDialogLoaded;
+ dialogOverlay = content.gSubDialog._topDialog._overlay;
+ ok(!BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is visible.");
+ return win;
+ }
+
+ function acceptLanguagesSubdialog(win) {
+ const button = win.document.querySelector("dialog").getButton("accept");
+ button.doCommand();
+ }
+
+ ok(BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is invisible.");
+ let win = await languagesSubdialogOpened();
+ ok(
+ win.document.getElementById("spoofEnglish").hidden,
+ "The 'Request English' checkbox is hidden."
+ );
+ acceptLanguagesSubdialog(win);
+ ok(BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is invisible.");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["intl.accept_languages", "en-US,en-XX,foo"]],
+ });
+ win = await languagesSubdialogOpened();
+ let activeLanguages = win.document.getElementById("activeLanguages").children;
+ ok(
+ activeLanguages[0].id == "en-us",
+ "The ID for 'en-US' locale code is correctly set."
+ );
+ ok(
+ activeLanguages[0].firstChild.value == "English (United States) [en-us]",
+ "The name for known 'en-US' locale code is correctly resolved."
+ );
+ ok(
+ activeLanguages[1].id == "en-xx",
+ "The ID for 'en-XX' locale code is correctly set."
+ );
+ ok(
+ activeLanguages[1].firstChild.value == "English [en-xx]",
+ "The name for unknown 'en-XX' locale code is resolved using 'en'."
+ );
+ ok(
+ activeLanguages[2].firstChild.value == " [foo]",
+ "The name for unknown 'foo' locale code is empty."
+ );
+ acceptLanguagesSubdialog(win);
+ await SpecialPowers.popPrefEnv();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", true],
+ ["privacy.spoof_english", 0],
+ ],
+ });
+
+ win = await languagesSubdialogOpened();
+ ok(
+ !win.document.getElementById("spoofEnglish").hidden,
+ "The 'Request English' checkbox isn't hidden."
+ );
+ ok(
+ !win.document.getElementById("spoofEnglish").checked,
+ "The 'Request English' checkbox isn't checked."
+ );
+ is(
+ win.Preferences.get("privacy.spoof_english").value,
+ 0,
+ "The privacy.spoof_english pref is set to 0."
+ );
+
+ win.document.getElementById("spoofEnglish").checked = true;
+ win.document.getElementById("spoofEnglish").doCommand();
+ ok(
+ win.document.getElementById("spoofEnglish").checked,
+ "The 'Request English' checkbox is checked."
+ );
+ is(
+ win.Preferences.get("privacy.spoof_english").value,
+ 2,
+ "The privacy.spoof_english pref is set to 2."
+ );
+ acceptLanguagesSubdialog(win);
+
+ win = await languagesSubdialogOpened();
+ ok(
+ !win.document.getElementById("spoofEnglish").hidden,
+ "The 'Request English' checkbox isn't hidden."
+ );
+ ok(
+ win.document.getElementById("spoofEnglish").checked,
+ "The 'Request English' checkbox is checked."
+ );
+ is(
+ win.Preferences.get("privacy.spoof_english").value,
+ 2,
+ "The privacy.spoof_english pref is set to 2."
+ );
+
+ win.document.getElementById("spoofEnglish").checked = false;
+ win.document.getElementById("spoofEnglish").doCommand();
+ ok(
+ !win.document.getElementById("spoofEnglish").checked,
+ "The 'Request English' checkbox isn't checked."
+ );
+ is(
+ win.Preferences.get("privacy.spoof_english").value,
+ 1,
+ "The privacy.spoof_english pref is set to 1."
+ );
+ acceptLanguagesSubdialog(win);
+
+ win = await languagesSubdialogOpened();
+ ok(
+ !win.document.getElementById("spoofEnglish").hidden,
+ "The 'Request English' checkbox isn't hidden."
+ );
+ ok(
+ !win.document.getElementById("spoofEnglish").checked,
+ "The 'Request English' checkbox isn't checked."
+ );
+ is(
+ win.Preferences.get("privacy.spoof_english").value,
+ 1,
+ "The privacy.spoof_english pref is set to 1."
+ );
+ acceptLanguagesSubdialog(win);
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_layersacceleration.js b/browser/components/preferences/tests/browser_layersacceleration.js
new file mode 100644
index 0000000000..982a32c94d
--- /dev/null
+++ b/browser/components/preferences/tests/browser_layersacceleration.js
@@ -0,0 +1,36 @@
+add_task(async function () {
+ // We must temporarily disable `Once` StaticPrefs check for the duration of
+ // this test (see bug 1556131). We must do so in a separate operation as
+ // pushPrefEnv doesn't set the preferences in the order one could expect.
+ await SpecialPowers.pushPrefEnv({
+ set: [["preferences.force-disable.check.once.policy", true]],
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["gfx.direct2d.disabled", false],
+ ["layers.acceleration.disabled", false],
+ ],
+ });
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let checkbox = doc.querySelector("#allowHWAccel");
+ is(
+ !checkbox.checked,
+ Services.prefs.getBoolPref("layers.acceleration.disabled"),
+ "checkbox should represent inverted pref value before clicking on checkbox"
+ );
+
+ checkbox.click();
+
+ is(
+ !checkbox.checked,
+ Services.prefs.getBoolPref("layers.acceleration.disabled"),
+ "checkbox should represent inverted pref value after clicking on checkbox"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_localSearchShortcuts.js b/browser/components/preferences/tests/browser_localSearchShortcuts.js
new file mode 100644
index 0000000000..0b8e170cc1
--- /dev/null
+++ b/browser/components/preferences/tests/browser_localSearchShortcuts.js
@@ -0,0 +1,309 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Checks the local shortcut rows in the engines list of the search pane.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+let gTree;
+
+add_setup(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
+ leaveOpen: true,
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ Assert.equal(
+ prefs.selectedPane,
+ "paneSearch",
+ "Sanity check: Search pane is selected by default"
+ );
+
+ gTree = gBrowser.contentDocument.querySelector("#engineList");
+ gTree.scrollIntoView();
+ gTree.focus();
+});
+
+// The rows should be visible and checked by default.
+add_task(async function visible() {
+ await checkRowVisibility(true);
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ Assert.equal(
+ gTree.view.getCellValue(row, gTree.columns.getNamedColumn("engineShown")),
+ "true",
+ "Row is checked initially"
+ );
+ });
+});
+
+// Toggling the browser.urlbar.shortcuts.* prefs should toggle the corresponding
+// checkboxes in the rows.
+add_task(async function syncFromPrefs() {
+ let col = gTree.columns.getNamedColumn("engineShown");
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ Assert.equal(
+ gTree.view.getCellValue(row, col),
+ "true",
+ "Row is checked initially"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [[getUrlbarPrefName(shortcut.pref), false]],
+ });
+ Assert.equal(
+ gTree.view.getCellValue(row, col),
+ "false",
+ "Row is unchecked after disabling pref"
+ );
+ await SpecialPowers.popPrefEnv();
+ Assert.equal(
+ gTree.view.getCellValue(row, col),
+ "true",
+ "Row is checked after re-enabling pref"
+ );
+ });
+});
+
+// Pressing the space key while a row is selected should toggle its checkbox
+// and pref.
+add_task(async function syncToPrefs_spaceKey() {
+ let col = gTree.columns.getNamedColumn("engineShown");
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ Assert.ok(
+ UrlbarPrefs.get(shortcut.pref),
+ "Sanity check: Pref is enabled initially"
+ );
+ Assert.equal(
+ gTree.view.getCellValue(row, col),
+ "true",
+ "Row is checked initially"
+ );
+ gTree.view.selection.select(row);
+ EventUtils.synthesizeKey(" ", {}, gTree.ownerGlobal);
+ Assert.ok(
+ !UrlbarPrefs.get(shortcut.pref),
+ "Pref is disabled after pressing space key"
+ );
+ Assert.equal(
+ gTree.view.getCellValue(row, col),
+ "false",
+ "Row is unchecked after pressing space key"
+ );
+ Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref));
+ });
+});
+
+// Clicking the checkbox in a local shortcut row should toggle the checkbox and
+// pref.
+add_task(async function syncToPrefs_click() {
+ let col = gTree.columns.getNamedColumn("engineShown");
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ Assert.ok(
+ UrlbarPrefs.get(shortcut.pref),
+ "Sanity check: Pref is enabled initially"
+ );
+ Assert.equal(
+ gTree.view.getCellValue(row, col),
+ "true",
+ "Row is checked initially"
+ );
+
+ let rect = gTree.getCoordsForCellItem(row, col, "cell");
+ let x = rect.x + rect.width / 2;
+ let y = rect.y + rect.height / 2;
+ EventUtils.synthesizeMouse(gTree.body, x, y, {}, gTree.ownerGlobal);
+
+ Assert.ok(
+ !UrlbarPrefs.get(shortcut.pref),
+ "Pref is disabled after clicking checkbox"
+ );
+ Assert.equal(
+ gTree.view.getCellValue(row, col),
+ "false",
+ "Row is unchecked after clicking checkbox"
+ );
+ Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref));
+ });
+});
+
+// The keyword column should not be editable according to isEditable().
+add_task(async function keywordNotEditable_isEditable() {
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ Assert.ok(
+ !gTree.view.isEditable(
+ row,
+ gTree.columns.getNamedColumn("engineKeyword")
+ ),
+ "Keyword column is not editable"
+ );
+ });
+});
+
+// Pressing the enter key while a row is selected shouldn't allow the keyword to
+// be edited.
+add_task(async function keywordNotEditable_enterKey() {
+ let col = gTree.columns.getNamedColumn("engineKeyword");
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ Assert.ok(
+ shortcut.restrict,
+ "Sanity check: Shortcut restriction char is non-empty"
+ );
+ Assert.equal(
+ gTree.view.getCellText(row, col),
+ shortcut.restrict,
+ "Sanity check: Keyword column has correct restriction char initially"
+ );
+
+ gTree.view.selection.select(row);
+ EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal);
+ EventUtils.sendString("newkeyword");
+ EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal);
+
+ // Wait a moment to allow for any possible asynchronicity.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+
+ Assert.equal(
+ gTree.view.getCellText(row, col),
+ shortcut.restrict,
+ "Keyword column is still restriction char"
+ );
+ });
+});
+
+// Double-clicking the keyword column shouldn't allow the keyword to be edited.
+add_task(async function keywordNotEditable_click() {
+ let col = gTree.columns.getNamedColumn("engineKeyword");
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ Assert.ok(
+ shortcut.restrict,
+ "Sanity check: Shortcut restriction char is non-empty"
+ );
+ Assert.equal(
+ gTree.view.getCellText(row, col),
+ shortcut.restrict,
+ "Sanity check: Keyword column has correct restriction char initially"
+ );
+
+ let rect = gTree.getCoordsForCellItem(row, col, "text");
+ let x = rect.x + rect.width / 2;
+ let y = rect.y + rect.height / 2;
+
+ let promise = BrowserTestUtils.waitForEvent(gTree, "dblclick");
+
+ // Click once to select the row.
+ EventUtils.synthesizeMouse(
+ gTree.body,
+ x,
+ y,
+ { clickCount: 1 },
+ gTree.ownerGlobal
+ );
+
+ // Now double-click the keyword column.
+ EventUtils.synthesizeMouse(
+ gTree.body,
+ x,
+ y,
+ { clickCount: 2 },
+ gTree.ownerGlobal
+ );
+
+ await promise;
+
+ EventUtils.sendString("newkeyword");
+ EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal);
+
+ // Wait a moment to allow for any possible asynchronicity.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+
+ Assert.equal(
+ gTree.view.getCellText(row, col),
+ shortcut.restrict,
+ "Keyword column is still restriction char"
+ );
+ });
+});
+
+/**
+ * Asserts that the engine and local shortcut rows are present in the tree.
+ */
+async function checkRowVisibility() {
+ let engines = await Services.search.getVisibleEngines();
+
+ Assert.equal(
+ gTree.view.rowCount,
+ engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length,
+ "Expected number of tree rows"
+ );
+
+ // Check the engine rows.
+ for (let row = 0; row < engines.length; row++) {
+ let engine = engines[row];
+ let text = gTree.view.getCellText(
+ row,
+ gTree.columns.getNamedColumn("engineName")
+ );
+ Assert.equal(
+ text,
+ engine.name,
+ `Sanity check: Tree row ${row} has expected engine name`
+ );
+ }
+
+ // Check the shortcut rows.
+ await forEachLocalShortcutRow(async (row, shortcut) => {
+ let text = gTree.view.getCellText(
+ row,
+ gTree.columns.getNamedColumn("engineName")
+ );
+ let name = UrlbarUtils.getResultSourceName(shortcut.source);
+ let l10nName = await gTree.ownerDocument.l10n.formatValue(
+ `urlbar-search-mode-${name}`
+ );
+ Assert.ok(l10nName, "Sanity check: l10n name is non-empty");
+ Assert.equal(text, l10nName, `Tree row ${row} has expected shortcut name`);
+ });
+}
+
+/**
+ * Calls a callback for each local shortcut row in the tree.
+ *
+ * @param {function} callback
+ * Called for each local shortcut row like: callback(rowIndex, shortcutObject)
+ */
+async function forEachLocalShortcutRow(callback) {
+ let engines = await Services.search.getVisibleEngines();
+ for (let i = 0; i < UrlbarUtils.LOCAL_SEARCH_MODES.length; i++) {
+ let shortcut = UrlbarUtils.LOCAL_SEARCH_MODES[i];
+ let row = engines.length + i;
+ // These tests assume LOCAL_SEARCH_MODES are enabled, this can be removed
+ // when we enable QuickActions. We cant just enable the pref in browser.ini
+ // as this test calls clearUserPref.
+ if (shortcut.pref == "shortcuts.quickactions") {
+ continue;
+ }
+ await callback(row, shortcut);
+ }
+}
+
+/**
+ * Prepends the `browser.urlbar.` branch to the given relative pref.
+ *
+ * @param {string} relativePref
+ * A pref name relative to the `browser.urlbar.`.
+ * @returns {string}
+ * The full pref name with `browser.urlbar.` prepended.
+ */
+function getUrlbarPrefName(relativePref) {
+ return `browser.urlbar.${relativePref}`;
+}
diff --git a/browser/components/preferences/tests/browser_moreFromMozilla.js b/browser/components/preferences/tests/browser_moreFromMozilla.js
new file mode 100644
index 0000000000..0c9b6e2b88
--- /dev/null
+++ b/browser/components/preferences/tests/browser_moreFromMozilla.js
@@ -0,0 +1,380 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+async function clearPolicies() {
+ // Ensure no active policies are set
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+}
+
+// The Relay promo is only shown if the default FxA instance is detected, and
+// tests override it to a dummy address, so we need to make the dummy address
+// appear like it's the default (using the actual default instance might cause a
+// remote connection, crashing the test harness).
+add_setup(mockDefaultFxAInstance);
+
+add_task(async function testDefaultUIWithoutTemplatePref() {
+ await clearPolicies();
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+ let tab = gBrowser.selectedTab;
+
+ let moreFromMozillaCategory = doc.getElementById(
+ "category-more-from-mozilla"
+ );
+ ok(moreFromMozillaCategory, "The category exists");
+ ok(!moreFromMozillaCategory.hidden, "The category is not hidden");
+
+ moreFromMozillaCategory.click();
+
+ let productCards = doc.querySelectorAll(".mozilla-product-item.simple");
+ Assert.ok(productCards, "Default UI uses simple template");
+ Assert.equal(productCards.length, 3, "3 product cards displayed");
+
+ const expectedUrl = "https://www.mozilla.org/firefox/browsers/mobile/";
+ let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, url =>
+ url.startsWith(expectedUrl)
+ );
+ let mobileLink = doc.getElementById("default-fxMobile");
+ mobileLink.click();
+ let openedTab = await tabOpened;
+ Assert.ok(gBrowser.selectedBrowser.documentURI.spec.startsWith(expectedUrl));
+
+ let searchParams = new URL(gBrowser.selectedBrowser.documentURI.spec)
+ .searchParams;
+ Assert.equal(
+ searchParams.get("utm_source"),
+ "about-prefs",
+ "expected utm_source sent"
+ );
+ Assert.equal(
+ searchParams.get("utm_campaign"),
+ "morefrommozilla",
+ "utm_campaign set"
+ );
+ Assert.equal(
+ searchParams.get("utm_medium"),
+ "firefox-desktop",
+ "utm_medium set"
+ );
+ Assert.equal(
+ searchParams.get("utm_content"),
+ "default-global",
+ "default utm_content set"
+ );
+ Assert.ok(
+ !searchParams.has("entrypoint_variation"),
+ "entrypoint_variation should not be set"
+ );
+ Assert.ok(
+ !searchParams.has("entrypoint_experiment"),
+ "entrypoint_experiment should not be set"
+ );
+ BrowserTestUtils.removeTab(openedTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testDefaulEmailClick() {
+ await clearPolicies();
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+ let tab = gBrowser.selectedTab;
+
+ let moreFromMozillaCategory = doc.getElementById(
+ "category-more-from-mozilla"
+ );
+ moreFromMozillaCategory.click();
+
+ const expectedUrl = "https://www.mozilla.org/firefox/mobile/get-app/?v=mfm";
+ let sendEmailLink = doc.getElementById("default-qr-code-send-email");
+
+ Assert.ok(
+ sendEmailLink.href.startsWith(expectedUrl),
+ `Expected URL ${sendEmailLink.href}`
+ );
+
+ let searchParams = new URL(sendEmailLink.href).searchParams;
+ Assert.equal(searchParams.get("v"), "mfm", "expected send email param set");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Test that we don't show moreFromMozilla pane when it's disabled.
+ */
+add_task(async function testwhenPrefDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.moreFromMozilla", false]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+
+ let moreFromMozillaCategory = doc.getElementById(
+ "category-more-from-mozilla"
+ );
+ ok(moreFromMozillaCategory, "The category exists");
+ ok(moreFromMozillaCategory.hidden, "The category is hidden");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_aboutpreferences_event_telemetry() {
+ Services.telemetry.clearEvents();
+ Services.telemetry.setEventRecordingEnabled("aboutpreferences", true);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.moreFromMozilla", true]],
+ });
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let moreFromMozillaCategory = doc.getElementById(
+ "category-more-from-mozilla"
+ );
+
+ let clickedPromise = BrowserTestUtils.waitForEvent(
+ moreFromMozillaCategory,
+ "click"
+ );
+ moreFromMozillaCategory.click();
+ await clickedPromise;
+
+ TelemetryTestUtils.assertEvents(
+ [["aboutpreferences", "show", "initial", "paneGeneral"]],
+ { category: "aboutpreferences", method: "show", object: "initial" },
+ { clear: false }
+ );
+ TelemetryTestUtils.assertEvents(
+ [["aboutpreferences", "show", "click", "paneMoreFromMozilla"]],
+ { category: "aboutpreferences", method: "show", object: "click" },
+ { clear: false }
+ );
+ TelemetryTestUtils.assertNumberOfEvents(2);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_aboutpreferences_simple_template() {
+ await clearPolicies();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.preferences.moreFromMozilla", true],
+ ["browser.preferences.moreFromMozilla.template", "simple"],
+ ],
+ });
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let moreFromMozillaCategory = doc.getElementById(
+ "category-more-from-mozilla"
+ );
+
+ moreFromMozillaCategory.click();
+
+ let productCards = doc.querySelectorAll(".mozilla-product-item");
+ Assert.ok(productCards, "The product cards from simple template found");
+ Assert.equal(productCards.length, 3, "3 product cards displayed");
+
+ let qrCodeButtons = doc.querySelectorAll('.qr-code-box[hidden="false"]');
+ Assert.equal(qrCodeButtons.length, 1, "1 qr-code box displayed");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_aboutpreferences_clickBtnVPN() {
+ await clearPolicies();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.preferences.moreFromMozilla", true],
+ ["browser.preferences.moreFromMozilla.template", "simple"],
+ ],
+ });
+ await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let tab = gBrowser.selectedTab;
+
+ let productCards = doc.querySelectorAll(".mozilla-product-item.simple");
+ Assert.ok(productCards, "Simple template loaded");
+
+ const expectedUrl = "https://www.mozilla.org/products/vpn/";
+ let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, url =>
+ url.startsWith(expectedUrl)
+ );
+
+ let vpnButton = doc.getElementById("simple-mozillaVPN");
+ vpnButton.click();
+
+ let openedTab = await tabOpened;
+ Assert.ok(gBrowser.selectedBrowser.documentURI.spec.startsWith(expectedUrl));
+
+ let searchParams = new URL(gBrowser.selectedBrowser.documentURI.spec)
+ .searchParams;
+ Assert.equal(
+ searchParams.get("utm_source"),
+ "about-prefs",
+ "expected utm_source sent"
+ );
+ Assert.equal(
+ searchParams.get("utm_campaign"),
+ "morefrommozilla",
+ "utm_campaign set"
+ );
+ Assert.equal(
+ searchParams.get("utm_medium"),
+ "firefox-desktop",
+ "utm_medium set"
+ );
+ Assert.equal(
+ searchParams.get("utm_content"),
+ "fxvt-113-a-global",
+ "utm_content set"
+ );
+ Assert.equal(
+ searchParams.get("entrypoint_experiment"),
+ "morefrommozilla-experiment-1846",
+ "entrypoint_experiment set"
+ );
+ Assert.equal(
+ searchParams.get("entrypoint_variation"),
+ "treatment-simple",
+ "entrypoint_variation set"
+ );
+ BrowserTestUtils.removeTab(openedTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_aboutpreferences_clickBtnMobile() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.preferences.moreFromMozilla", true],
+ ["browser.preferences.moreFromMozilla.template", "simple"],
+ ],
+ });
+ await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let tab = gBrowser.selectedTab;
+
+ let productCards = doc.querySelectorAll("vbox.simple");
+ Assert.ok(productCards, "Simple template loaded");
+
+ const expectedUrl = "https://www.mozilla.org/firefox/browsers/mobile/";
+
+ let mobileUrl = new URL(doc.getElementById("simple-fxMobile").href);
+
+ Assert.ok(mobileUrl.href.startsWith(expectedUrl));
+
+ let searchParams = mobileUrl.searchParams;
+ Assert.equal(
+ searchParams.get("utm_source"),
+ "about-prefs",
+ "expected utm_source sent"
+ );
+ Assert.equal(
+ searchParams.get("utm_campaign"),
+ "morefrommozilla",
+ "utm_campaign set"
+ );
+ Assert.equal(
+ searchParams.get("utm_medium"),
+ "firefox-desktop",
+ "utm_medium set"
+ );
+ Assert.equal(
+ searchParams.get("utm_content"),
+ "fxvt-113-a-global",
+ "default-global",
+ "utm_content set"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_aboutpreferences_search() {
+ await clearPolicies();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.moreFromMozilla", true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI(null, {
+ leaveOpen: true,
+ });
+
+ await runSearchInput("Relay");
+
+ let doc = gBrowser.contentDocument;
+ let tab = gBrowser.selectedTab;
+
+ let productCards = doc.querySelectorAll(".mozilla-product-item");
+ Assert.equal(productCards.length, 3, "All products in the group are found");
+ let [mobile, vpn, relay] = productCards;
+ Assert.ok(BrowserTestUtils.is_hidden(mobile), "Mobile hidden");
+ Assert.ok(BrowserTestUtils.is_hidden(vpn), "VPN hidden");
+ Assert.ok(BrowserTestUtils.is_visible(relay), "Relay shown");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_aboutpreferences_clickBtnRelay() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.moreFromMozilla", true]],
+ });
+ await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let tab = gBrowser.selectedTab;
+
+ let expectedUrl = new URL("https://relay.firefox.com");
+ expectedUrl.searchParams.set("utm_source", "about-prefs");
+ expectedUrl.searchParams.set("utm_campaign", "morefrommozilla");
+ expectedUrl.searchParams.set("utm_medium", "firefox-desktop");
+ expectedUrl.searchParams.set("utm_content", "fxvt-113-a-global");
+ expectedUrl.searchParams.set(
+ "entrypoint_experiment",
+ "morefrommozilla-experiment-1846"
+ );
+ expectedUrl.searchParams.set("entrypoint_variation", "treatment-simple");
+
+ let tabOpened = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedUrl.toString(),
+ gBrowser,
+ channel => {
+ Assert.equal(
+ channel.originalURI.spec,
+ expectedUrl.toString(),
+ "URL matched"
+ );
+ return true;
+ }
+ );
+ doc.getElementById("simple-firefoxRelay").click();
+
+ await tabOpened;
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/preferences/tests/browser_moreFromMozilla_locales.js b/browser/components/preferences/tests/browser_moreFromMozilla_locales.js
new file mode 100644
index 0000000000..404e22b3ea
--- /dev/null
+++ b/browser/components/preferences/tests/browser_moreFromMozilla_locales.js
@@ -0,0 +1,331 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+let { Region } = ChromeUtils.importESModule(
+ "resource://gre/modules/Region.sys.mjs"
+);
+
+const initialHomeRegion = Region._home;
+const initialCurrentRegion = Region._current;
+
+// Helper to run tests for specific regions
+async function setupRegions(home, current) {
+ Region._setHomeRegion(home || "");
+ Region._setCurrentRegion(current || "");
+}
+
+function setLocale(language) {
+ Services.locale.availableLocales = [language];
+ Services.locale.requestedLocales = [language];
+}
+
+async function clearPolicies() {
+ // Ensure no active policies are set
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+}
+
+async function getPromoCards() {
+ await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let vpnPromoCard = doc.getElementById("mozilla-vpn");
+ let mobileCard = doc.getElementById("firefox-mobile");
+ let relayPromoCard = doc.getElementById("firefox-relay");
+
+ return {
+ vpnPromoCard,
+ mobileCard,
+ relayPromoCard,
+ };
+}
+
+let mockFxA, unmockFxA;
+
+// The Relay promo is only shown if the default FxA instance is detected, and
+// tests override it to a dummy address, so we need to make the dummy address
+// appear like it's the default (using the actual default instance might cause a
+// remote connection, crashing the test harness).
+add_setup(async function () {
+ let { mock, unmock } = await mockDefaultFxAInstance();
+ mockFxA = mock;
+ unmockFxA = unmock;
+});
+
+add_task(async function test_VPN_promo_enabled() {
+ await clearPolicies();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.preferences.moreFromMozilla", true],
+ ["browser.vpn_promo.enabled", true],
+ ],
+ });
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(vpnPromoCard, "The VPN promo is visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_VPN_promo_disabled() {
+ await clearPolicies();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.vpn_promo.enabled", false]],
+ });
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(!vpnPromoCard, "The VPN promo is not visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ Services.prefs.clearUserPref("browser.vpn_promo.enabled");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_VPN_promo_in_disallowed_home_region() {
+ await clearPolicies();
+ const disallowedRegion = "SY";
+
+ setupRegions(disallowedRegion);
+
+ // Promo should not show in disallowed regions even when vpn_promo pref is enabled
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.vpn_promo.enabled", true]],
+ });
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(!vpnPromoCard, "The VPN promo is not visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_VPN_promo_in_illegal_home_region() {
+ await clearPolicies();
+ const illegalRegion = "CN";
+
+ setupRegions(illegalRegion);
+
+ // Promo should not show in illegal regions even if the list of disallowed regions is somehow altered (though changing this preference is blocked)
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.vpn_promo.disallowedRegions", "SY, CU"]],
+ });
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(!vpnPromoCard, "The VPN promo is not visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_VPN_promo_in_disallowed_current_region() {
+ await clearPolicies();
+ const allowedRegion = "US";
+ const disallowedRegion = "SY";
+
+ setupRegions(allowedRegion, disallowedRegion);
+
+ // Promo should not show in disallowed regions even when vpn_promo pref is enabled
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.vpn_promo.enabled", true]],
+ });
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(!vpnPromoCard, "The VPN promo is not visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_VPN_promo_in_illegal_current_region() {
+ await clearPolicies();
+ const allowedRegion = "US";
+ const illegalRegion = "CN";
+
+ setupRegions(allowedRegion, illegalRegion);
+
+ // Promo should not show in illegal regions even if the list of disallowed regions is somehow altered (though changing this preference is blocked)
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.vpn_promo.disallowedRegions", "SY, CU"]],
+ });
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(!vpnPromoCard, "The VPN promo is not visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_aboutpreferences_partnerCNRepack() {
+ let defaultBranch = Services.prefs.getDefaultBranch(null);
+ defaultBranch.setCharPref("distribution.id", "MozillaOnline");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.preferences.moreFromMozilla", true],
+ ["browser.preferences.moreFromMozilla.template", "simple"],
+ ],
+ });
+ await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let tab = gBrowser.selectedTab;
+
+ let productCards = doc.querySelectorAll("vbox.simple");
+ Assert.ok(productCards, "Simple template loaded");
+
+ const expectedUrl = "https://www.firefox.com.cn/browsers/mobile/";
+
+ let link = doc.getElementById("simple-fxMobile");
+ Assert.ok(link.getAttribute("href").startsWith(expectedUrl));
+
+ defaultBranch.setCharPref("distribution.id", "");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_send_to_device_email_link_for_supported_locale() {
+ // Email is supported for Brazilian Portuguese
+ const supportedLocale = "pt-BR";
+ const initialLocale = Services.locale.appLocaleAsBCP47;
+ setLocale(supportedLocale);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.moreFromMozilla.template", "simple"]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let emailLink = doc.getElementById("simple-qr-code-send-email");
+
+ ok(!BrowserTestUtils.is_hidden(emailLink), "Email link should be visible");
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ setLocale(initialLocale); // revert changes to language
+});
+
+add_task(
+ async function test_send_to_device_email_link_for_unsupported_locale() {
+ // Email is not supported for Afrikaans
+ const unsupportedLocale = "af";
+ const initialLocale = Services.locale.appLocaleAsBCP47;
+ setLocale(unsupportedLocale);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.moreFromMozilla.template", "simple"]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", {
+ leaveOpen: true,
+ });
+
+ let doc = gBrowser.contentDocument;
+ let emailLink = doc.getElementById("simple-qr-code-send-email");
+
+ ok(BrowserTestUtils.is_hidden(emailLink), "Email link should be hidden");
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ setLocale(initialLocale); // revert changes to language
+ }
+);
+
+add_task(
+ async function test_VPN_promo_in_unsupported_current_region_with_supported_home_region() {
+ await clearPolicies();
+ const supportedRegion = "US";
+ const unsupportedRegion = "LY";
+
+ setupRegions(supportedRegion, unsupportedRegion);
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(vpnPromoCard, "The VPN promo is visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+);
+
+add_task(
+ async function test_VPN_promo_in_supported_current_region_with_unsupported_home_region() {
+ await clearPolicies();
+ const supportedRegion = "US";
+ const unsupportedRegion = "LY";
+
+ setupRegions(unsupportedRegion, supportedRegion);
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+
+ ok(vpnPromoCard, "The VPN promo is visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+);
+
+add_task(async function test_VPN_promo_with_active_enterprise_policy() {
+ // set up an arbitrary enterprise policy
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ EnableTrackingProtection: {
+ Value: true,
+ },
+ },
+ });
+
+ let { vpnPromoCard, mobileCard } = await getPromoCards();
+ ok(!vpnPromoCard, "The VPN promo is not visible");
+ ok(mobileCard, "The Mobile promo is visible");
+
+ setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions
+ await clearPolicies();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_relay_promo_with_supported_fxa_server() {
+ await clearPolicies();
+
+ let { relayPromoCard } = await getPromoCards();
+ ok(relayPromoCard, "The Relay promo is visible");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_relay_promo_with_unsupported_fxa_server() {
+ await clearPolicies();
+ // Set the default pref value to something other than the current value so it
+ // will appear to be user-set and treated as invalid (actually setting the
+ // pref would cause a remote connection and crash the test harness)
+ unmockFxA();
+
+ let { relayPromoCard } = await getPromoCards();
+ ok(!relayPromoCard, "The Relay promo is not visible");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ mockFxA();
+});
diff --git a/browser/components/preferences/tests/browser_newtab_menu.js b/browser/components/preferences/tests/browser_newtab_menu.js
new file mode 100644
index 0000000000..774c9dd756
--- /dev/null
+++ b/browser/components/preferences/tests/browser_newtab_menu.js
@@ -0,0 +1,38 @@
+add_task(async function newtabPreloaded() {
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+
+ const { contentDocument: doc, contentWindow } = gBrowser;
+ function dispatchMenuItemCommand(menuItem) {
+ const cmdEvent = doc.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ contentWindow,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ 0
+ );
+ menuItem.dispatchEvent(cmdEvent);
+ }
+
+ const menuHome = doc.querySelector(`#newTabMode menuitem[value="0"]`);
+ const menuBlank = doc.querySelector(`#newTabMode menuitem[value="1"]`);
+ ok(menuHome.selected, "The first item, Home (default), is selected.");
+ ok(NewTabPagePreloading.enabled, "Default Home allows preloading.");
+
+ dispatchMenuItemCommand(menuBlank);
+ ok(menuBlank.selected, "The second item, Blank, is selected.");
+ ok(!NewTabPagePreloading.enabled, "Non-Home prevents preloading.");
+
+ dispatchMenuItemCommand(menuHome);
+ ok(menuHome.selected, "The first item, Home, is selected again.");
+ ok(NewTabPagePreloading.enabled, "Default Home allows preloading again.");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_notifications_do_not_disturb.js b/browser/components/preferences/tests/browser_notifications_do_not_disturb.js
new file mode 100644
index 0000000000..afc31b9041
--- /dev/null
+++ b/browser/components/preferences/tests/browser_notifications_do_not_disturb.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+registerCleanupFunction(function () {
+ while (gBrowser.tabs[1]) {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ }
+});
+
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let notificationsDoNotDisturbBox = doc.getElementById(
+ "notificationsDoNotDisturbBox"
+ );
+ if (notificationsDoNotDisturbBox.hidden) {
+ todo(false, "Do not disturb is not available on this platform");
+ return;
+ }
+
+ let alertService;
+ try {
+ alertService = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIAlertsDoNotDisturb);
+ } catch (ex) {
+ ok(true, "Do not disturb is not available on this platform: " + ex.message);
+ return;
+ }
+
+ let checkbox = doc.getElementById("notificationsDoNotDisturb");
+ ok(!checkbox.checked, "Checkbox should not be checked by default");
+ ok(
+ !alertService.manualDoNotDisturb,
+ "Do not disturb should be off by default"
+ );
+
+ let checkboxChanged = BrowserTestUtils.waitForEvent(checkbox, "command");
+ checkbox.click();
+ await checkboxChanged;
+ ok(
+ alertService.manualDoNotDisturb,
+ "Do not disturb should be enabled when checked"
+ );
+
+ checkboxChanged = BrowserTestUtils.waitForEvent(checkbox, "command");
+ checkbox.click();
+ await checkboxChanged;
+ ok(
+ !alertService.manualDoNotDisturb,
+ "Do not disturb should be disabled when unchecked"
+ );
+});
diff --git a/browser/components/preferences/tests/browser_open_download_preferences.js b/browser/components/preferences/tests/browser_open_download_preferences.js
new file mode 100644
index 0000000000..794d2ebb05
--- /dev/null
+++ b/browser/components/preferences/tests/browser_open_download_preferences.js
@@ -0,0 +1,288 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { HandlerServiceTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/HandlerServiceTestUtils.sys.mjs"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+async function selectPdfCategoryItem() {
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ info("Preferences page opened on the general pane.");
+
+ await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList;
+ info("Apps list loaded.");
+
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let container = win.document.getElementById("handlersView");
+ let pdfCategory = container.querySelector(
+ "richlistitem[type='application/pdf']"
+ );
+
+ pdfCategory.closest("richlistbox").selectItem(pdfCategory);
+ Assert.ok(pdfCategory.selected, "Should be able to select our item.");
+
+ return pdfCategory;
+}
+
+async function selectItemInPopup(item, list) {
+ let popup = list.menupopup;
+ let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ // Synthesizing the mouse on the .actionsMenu menulist somehow just selects
+ // the top row. Probably something to do with the multiple layers of anon
+ // content - workaround by using the `.open` setter instead.
+ list.open = true;
+ await popupShown;
+ let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+
+ item.click();
+ popup.hidePopup();
+ await popupHidden;
+ return item;
+}
+
+function downloadHadFinished(publicList) {
+ return new Promise(resolve => {
+ publicList.addView({
+ onDownloadChanged(download) {
+ if (download.succeeded || download.error) {
+ publicList.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+}
+
+async function removeTheFile(download) {
+ Assert.ok(
+ await IOUtils.exists(download.target.path),
+ "The file should have been downloaded."
+ );
+
+ try {
+ info("removing " + download.target.path);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ info("The file " + download.target.path + " is not removed, " + ex);
+ }
+}
+
+add_task(async function alwaysAskPreferenceWorks() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ let pdfCategory = await selectPdfCategoryItem();
+ let list = pdfCategory.querySelector(".actionsMenu");
+
+ let alwaysAskItem = list.querySelector(
+ `menuitem[action='${Ci.nsIHandlerInfo.alwaysAsk}']`
+ );
+
+ await selectItemInPopup(alwaysAskItem, list);
+ Assert.equal(
+ list.selectedItem,
+ alwaysAskItem,
+ "Should have selected 'always ask' for pdf"
+ );
+ let alwaysAskBeforeHandling = HandlerServiceTestUtils.getHandlerInfo(
+ pdfCategory.getAttribute("type")
+ ).alwaysAskBeforeHandling;
+ Assert.ok(
+ alwaysAskBeforeHandling,
+ "Should have turned on 'always asking before handling'"
+ );
+
+ let domWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "empty_pdf_file.pdf",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ let domWindow = await domWindowPromise;
+ let dialog = domWindow.document.querySelector("#unknownContentType");
+ let button = dialog.getButton("cancel");
+
+ await TestUtils.waitForCondition(
+ () => !button.disabled,
+ "Wait for Cancel button to get enabled"
+ );
+ Assert.ok(dialog, "Dialog should be shown");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function handleInternallyPreferenceWorks() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ let pdfCategory = await selectPdfCategoryItem();
+ let list = pdfCategory.querySelector(".actionsMenu");
+
+ let handleInternallyItem = list.querySelector(
+ `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']`
+ );
+
+ await selectItemInPopup(handleInternallyItem, list);
+ Assert.equal(
+ list.selectedItem,
+ handleInternallyItem,
+ "Should have selected 'handle internally' for pdf"
+ );
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "empty_pdf_file.pdf",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ await ContentTask.spawn(loadingTab.linkedBrowser, null, async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "complete"
+ );
+ Assert.ok(
+ content.document.querySelector("div#viewer"),
+ "document content has viewer UI"
+ );
+ });
+
+ BrowserTestUtils.removeTab(loadingTab);
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function saveToDiskPreferenceWorks() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ let pdfCategory = await selectPdfCategoryItem();
+ let list = pdfCategory.querySelector(".actionsMenu");
+
+ let saveToDiskItem = list.querySelector(
+ `menuitem[action='${Ci.nsIHandlerInfo.saveToDisk}']`
+ );
+
+ await selectItemInPopup(saveToDiskItem, list);
+ Assert.equal(
+ list.selectedItem,
+ saveToDiskItem,
+ "Should have selected 'save to disk' for pdf"
+ );
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+
+ let downloadFinishedPromise = downloadHadFinished(publicList);
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "empty_pdf_file.pdf",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ let download = await downloadFinishedPromise;
+ BrowserTestUtils.removeTab(loadingTab);
+
+ await removeTheFile(download);
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function useSystemDefaultPreferenceWorks() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ let pdfCategory = await selectPdfCategoryItem();
+ let list = pdfCategory.querySelector(".actionsMenu");
+
+ let useSystemDefaultItem = list.querySelector(
+ `menuitem[action='${Ci.nsIHandlerInfo.useSystemDefault}']`
+ );
+
+ // Whether there's a "use default" item depends on the OS, there might not be a system default viewer.
+ if (!useSystemDefaultItem) {
+ info(
+ "No 'Use default' item, so no testing for setting 'use system default' preference"
+ );
+ gBrowser.removeCurrentTab();
+ return;
+ }
+
+ await selectItemInPopup(useSystemDefaultItem, list);
+ Assert.equal(
+ list.selectedItem,
+ useSystemDefaultItem,
+ "Should have selected 'use system default' for pdf"
+ );
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+
+ let downloadFinishedPromise = downloadHadFinished(publicList);
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "empty_pdf_file.pdf",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ info("Downloading had finished");
+ let download = await downloadFinishedPromise;
+
+ info("Waiting until DownloadIntegration.launchFile is called");
+ await waitForLaunchFileCalled;
+
+ DownloadIntegration.launchFile = oldLaunchFile;
+
+ await removeTheFile(download);
+
+ BrowserTestUtils.removeTab(loadingTab);
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_open_migration_wizard.js b/browser/components/preferences/tests/browser_open_migration_wizard.js
new file mode 100644
index 0000000000..c2c18b35ef
--- /dev/null
+++ b/browser/components/preferences/tests/browser_open_migration_wizard.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the "Import Data" button in the "Import Browser Data" section of
+ * the General pane of about:preferences launches the Migration Wizard.
+ */
+add_task(async function test_open_migration_wizard() {
+ const BUTTON_ID = "data-migration";
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#general" },
+ async function (browser) {
+ let button = browser.contentDocument.getElementById(BUTTON_ID);
+
+ // First, we'll test the legacy Migration Wizard.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.migrate.content-modal.enabled", false]],
+ });
+
+ let migrationWizardWindow = BrowserTestUtils.domWindowOpenedAndLoaded(
+ null,
+ win => {
+ let type = win.document.documentElement.getAttribute("windowtype");
+ if (type == "Browser:MigrationWizard") {
+ Assert.ok(true, "Saw legacy Migration Wizard window open.");
+ return true;
+ }
+
+ return false;
+ }
+ );
+
+ button.click();
+ let win = await migrationWizardWindow;
+ await BrowserTestUtils.closeWindow(win);
+
+ // Next, we'll test the new Migration Wizard.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.migrate.content-modal.enabled", true]],
+ });
+
+ let wizardReady = BrowserTestUtils.waitForEvent(
+ browser.contentWindow,
+ "MigrationWizard:Ready"
+ );
+ button.click();
+ await wizardReady;
+ Assert.ok(true, "Saw the new Migration Wizard dialog open.");
+ }
+ );
+});
diff --git a/browser/components/preferences/tests/browser_password_management.js b/browser/components/preferences/tests/browser_password_management.js
new file mode 100644
index 0000000000..d84c88dc08
--- /dev/null
+++ b/browser/components/preferences/tests/browser_password_management.js
@@ -0,0 +1,43 @@
+"use strict";
+
+const { LoginTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+var passwordsDialog;
+
+add_task(async function test_openPasswordManagement() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:logins");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let doc = content.document;
+
+ let savePasswordCheckBox = doc.getElementById("savePasswords");
+ Assert.ok(
+ !savePasswordCheckBox.checked,
+ "Save Password CheckBox should be unchecked by default"
+ );
+
+ let showPasswordsButton = doc.getElementById("showPasswords");
+ showPasswordsButton.click();
+ });
+
+ let tab = await tabOpenPromise;
+ ok(tab, "Tab opened");
+
+ // check telemetry events while we are in here
+ await LoginTestUtils.telemetry.waitForEventCount(1);
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "open_management", "preferences"]],
+ { category: "pwmgr", method: "open_management" },
+ { clear: true, process: "content" }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_pdf_disabled.js b/browser/components/preferences/tests/browser_pdf_disabled.js
new file mode 100644
index 0000000000..5b814b39e9
--- /dev/null
+++ b/browser/components/preferences/tests/browser_pdf_disabled.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks that pdf always appears in the applications list even
+// both a customized handler doesn't exist and when the internal viewer is
+// not enabled.
+add_task(async function pdfIsAlwaysPresent() {
+ // Try again with the pdf viewer enabled and disabled.
+ for (let test of ["enabled", "disabled"]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["pdfjs.disabled", test == "disabled"]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+
+ let win = gBrowser.selectedBrowser.contentWindow;
+
+ let container = win.document.getElementById("handlersView");
+
+ // First, find the PDF item.
+ let pdfItem = container.querySelector(
+ "richlistitem[type='application/pdf']"
+ );
+ Assert.ok(pdfItem, "pdfItem is present in handlersView when " + test);
+ if (pdfItem) {
+ pdfItem.scrollIntoView({ block: "center" });
+ pdfItem.closest("richlistbox").selectItem(pdfItem);
+
+ // Open its menu
+ let list = pdfItem.querySelector(".actionsMenu");
+ let popup = list.menupopup;
+ let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(list, {}, win);
+ await popupShown;
+
+ let handleInternallyItem = list.querySelector(
+ `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']`
+ );
+
+ is(
+ test == "enabled",
+ !!handleInternallyItem,
+ "handle internally is present when " + test
+ );
+ }
+
+ gBrowser.removeCurrentTab();
+ }
+});
diff --git a/browser/components/preferences/tests/browser_performance.js b/browser/components/preferences/tests/browser_performance.js
new file mode 100644
index 0000000000..bd7ff70c51
--- /dev/null
+++ b/browser/components/preferences/tests/browser_performance.js
@@ -0,0 +1,300 @@
+const DEFAULT_HW_ACCEL_PREF = Services.prefs
+ .getDefaultBranch(null)
+ .getBoolPref("layers.acceleration.disabled");
+const DEFAULT_PROCESS_COUNT = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref("dom.ipc.processCount");
+
+add_task(async function () {
+ // We must temporarily disable `Once` StaticPrefs check for the duration of
+ // this test (see bug 1556131). We must do so in a separate operation as
+ // pushPrefEnv doesn't set the preferences in the order one could expect.
+ await SpecialPowers.pushPrefEnv({
+ set: [["preferences.force-disable.check.once.policy", true]],
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF],
+ ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT],
+ ["browser.preferences.defaultPerformanceSettings.enabled", true],
+ ],
+ });
+});
+
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let useRecommendedPerformanceSettings = doc.querySelector(
+ "#useRecommendedPerformanceSettings"
+ );
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ true,
+ "pref value should be true before clicking on checkbox"
+ );
+ ok(
+ useRecommendedPerformanceSettings.checked,
+ "checkbox should be checked before clicking on checkbox"
+ );
+
+ useRecommendedPerformanceSettings.click();
+
+ let performanceSettings = doc.querySelector("#performanceSettings");
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section is shown"
+ );
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ false,
+ "pref value should be false after clicking on checkbox"
+ );
+ ok(
+ !useRecommendedPerformanceSettings.checked,
+ "checkbox should not be checked after clicking on checkbox"
+ );
+
+ let allowHWAccel = doc.querySelector("#allowHWAccel");
+ let allowHWAccelPref = Services.prefs.getBoolPref(
+ "layers.acceleration.disabled"
+ );
+ is(
+ allowHWAccelPref,
+ DEFAULT_HW_ACCEL_PREF,
+ "pref value should be the default value before clicking on checkbox"
+ );
+ is(
+ allowHWAccel.checked,
+ !DEFAULT_HW_ACCEL_PREF,
+ "checkbox should show the invert of the default value"
+ );
+
+ let contentProcessCount = doc.querySelector("#contentProcessCount");
+ is(
+ contentProcessCount.disabled,
+ false,
+ "process count control should be enabled"
+ );
+ is(
+ Services.prefs.getIntPref("dom.ipc.processCount"),
+ DEFAULT_PROCESS_COUNT,
+ "default pref value should be default value"
+ );
+ is(
+ contentProcessCount.selectedItem.value,
+ "" + DEFAULT_PROCESS_COUNT,
+ "selected item should be the default one"
+ );
+
+ allowHWAccel.click();
+ allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled");
+ is(
+ allowHWAccelPref,
+ !DEFAULT_HW_ACCEL_PREF,
+ "pref value should be opposite of the default value after clicking on checkbox"
+ );
+ is(
+ allowHWAccel.checked,
+ !allowHWAccelPref,
+ "checkbox should show the invert of the current value"
+ );
+
+ contentProcessCount.value = 7;
+ contentProcessCount.doCommand();
+ is(
+ Services.prefs.getIntPref("dom.ipc.processCount"),
+ 7,
+ "pref value should be 7"
+ );
+ is(contentProcessCount.selectedItem.value, "7", "selected item should be 7");
+
+ allowHWAccel.click();
+ allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled");
+ is(
+ allowHWAccelPref,
+ DEFAULT_HW_ACCEL_PREF,
+ "pref value should be the default value after clicking on checkbox"
+ );
+ is(
+ allowHWAccel.checked,
+ !allowHWAccelPref,
+ "checkbox should show the invert of the current value"
+ );
+
+ contentProcessCount.value = DEFAULT_PROCESS_COUNT;
+ contentProcessCount.doCommand();
+ is(
+ Services.prefs.getIntPref("dom.ipc.processCount"),
+ DEFAULT_PROCESS_COUNT,
+ "pref value should be default value"
+ );
+ is(
+ contentProcessCount.selectedItem.value,
+ "" + DEFAULT_PROCESS_COUNT,
+ "selected item should be default one"
+ );
+
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section should be still shown"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let useRecommendedPerformanceSettings = doc.querySelector(
+ "#useRecommendedPerformanceSettings"
+ );
+ let allowHWAccel = doc.querySelector("#allowHWAccel");
+ let contentProcessCount = doc.querySelector("#contentProcessCount");
+ let performanceSettings = doc.querySelector("#performanceSettings");
+
+ useRecommendedPerformanceSettings.click();
+ allowHWAccel.click();
+ contentProcessCount.value = 7;
+ contentProcessCount.doCommand();
+ useRecommendedPerformanceSettings.click();
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ true,
+ "pref value should be true before clicking on checkbox"
+ );
+ ok(
+ useRecommendedPerformanceSettings.checked,
+ "checkbox should be checked before clicking on checkbox"
+ );
+ is(
+ performanceSettings.hidden,
+ true,
+ "performance settings section should be still shown"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let performanceSettings = doc.querySelector("#performanceSettings");
+
+ is(
+ performanceSettings.hidden,
+ true,
+ "performance settings section should not be shown"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ false
+ );
+
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section should be shown"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ Services.prefs.setIntPref("dom.ipc.processCount", 7);
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+
+ let performanceSettings = doc.querySelector("#performanceSettings");
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section should be shown"
+ );
+
+ let contentProcessCount = doc.querySelector("#contentProcessCount");
+ is(
+ Services.prefs.getIntPref("dom.ipc.processCount"),
+ 7,
+ "pref value should be 7"
+ );
+ is(contentProcessCount.selectedItem.value, "7", "selected item should be 7");
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ Services.prefs.setBoolPref("layers.acceleration.disabled", true);
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+
+ let performanceSettings = doc.querySelector("#performanceSettings");
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section should be shown"
+ );
+
+ let allowHWAccel = doc.querySelector("#allowHWAccel");
+ is(
+ Services.prefs.getBoolPref("layers.acceleration.disabled"),
+ true,
+ "pref value is false"
+ );
+ ok(!allowHWAccel.checked, "checkbox should not be checked");
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_performance_content_process_limit.js b/browser/components/preferences/tests/browser_performance_content_process_limit.js
new file mode 100644
index 0000000000..7ac5c354bd
--- /dev/null
+++ b/browser/components/preferences/tests/browser_performance_content_process_limit.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.defaultPerformanceSettings.enabled", false]],
+ });
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+
+ let limitContentProcess = doc.querySelector("#limitContentProcess");
+ is(
+ limitContentProcess.hidden,
+ Services.appinfo.fissionAutostart,
+ "Limit Content Process should be hidden if fission is enabled and shown if it is not."
+ );
+
+ let contentProcessCount = doc.querySelector("#contentProcessCount");
+ is(
+ contentProcessCount.hidden,
+ Services.appinfo.fissionAutostart,
+ "Limit Content Count should be hidden if fission is enabled and shown if it is not."
+ );
+
+ let contentProcessCountEnabledDescription = doc.querySelector(
+ "#contentProcessCountEnabledDescription"
+ );
+ is(
+ contentProcessCountEnabledDescription.hidden,
+ Services.appinfo.fissionAutostart,
+ "Limit Content Process Enabled Description should be hidden if fission is enabled and shown if it is not."
+ );
+
+ let contentProcessCountDisabledDescription = doc.querySelector(
+ "#contentProcessCountDisabledDescription"
+ );
+ is(
+ contentProcessCountDisabledDescription.hidden,
+ Services.appinfo.fissionAutostart ||
+ Services.appinfo.browserTabsRemoteAutostart,
+ "Limit Content Process Disabled Description should be shown if e10s is disabled, and hidden otherwise."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_performance_e10srollout.js b/browser/components/preferences/tests/browser_performance_e10srollout.js
new file mode 100644
index 0000000000..1c2c57b6e7
--- /dev/null
+++ b/browser/components/preferences/tests/browser_performance_e10srollout.js
@@ -0,0 +1,164 @@
+const DEFAULT_HW_ACCEL_PREF = Services.prefs
+ .getDefaultBranch(null)
+ .getBoolPref("layers.acceleration.disabled");
+const DEFAULT_PROCESS_COUNT = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref("dom.ipc.processCount");
+
+add_task(async function () {
+ // We must temporarily disable `Once` StaticPrefs check for the duration of
+ // this test (see bug 1556131). We must do so in a separate operation as
+ // pushPrefEnv doesn't set the preferences in the order one could expect.
+ await SpecialPowers.pushPrefEnv({
+ set: [["preferences.force-disable.check.once.policy", true]],
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF],
+ ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT],
+ ["browser.preferences.defaultPerformanceSettings.enabled", true],
+ ],
+ });
+});
+
+add_task(async function testPrefsAreDefault() {
+ Services.prefs.setIntPref("dom.ipc.processCount", DEFAULT_PROCESS_COUNT);
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let useRecommendedPerformanceSettings = doc.querySelector(
+ "#useRecommendedPerformanceSettings"
+ );
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ true,
+ "pref value should be true before clicking on checkbox"
+ );
+ ok(
+ useRecommendedPerformanceSettings.checked,
+ "checkbox should be checked before clicking on checkbox"
+ );
+
+ useRecommendedPerformanceSettings.click();
+
+ let performanceSettings = doc.querySelector("#performanceSettings");
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section is shown"
+ );
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ false,
+ "pref value should be false after clicking on checkbox"
+ );
+ ok(
+ !useRecommendedPerformanceSettings.checked,
+ "checkbox should not be checked after clicking on checkbox"
+ );
+
+ let contentProcessCount = doc.querySelector("#contentProcessCount");
+ is(
+ contentProcessCount.disabled,
+ false,
+ "process count control should be enabled"
+ );
+ is(
+ Services.prefs.getIntPref("dom.ipc.processCount"),
+ DEFAULT_PROCESS_COUNT,
+ "default pref should be default value"
+ );
+ is(
+ contentProcessCount.selectedItem.value,
+ "" + DEFAULT_PROCESS_COUNT,
+ "selected item should be the default one"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ Services.prefs.clearUserPref("dom.ipc.processCount");
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+});
+
+add_task(async function testPrefsSetByUser() {
+ const kNewCount = DEFAULT_PROCESS_COUNT - 2;
+
+ Services.prefs.setIntPref("dom.ipc.processCount", kNewCount);
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ false
+ );
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let performanceSettings = doc.querySelector("#performanceSettings");
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section is shown"
+ );
+
+ let contentProcessCount = doc.querySelector("#contentProcessCount");
+ is(
+ contentProcessCount.disabled,
+ false,
+ "process count control should be enabled"
+ );
+ is(
+ Services.prefs.getIntPref("dom.ipc.processCount"),
+ kNewCount,
+ "process count should be the set value"
+ );
+ is(
+ contentProcessCount.selectedItem.value,
+ "" + kNewCount,
+ "selected item should be the set one"
+ );
+
+ let useRecommendedPerformanceSettings = doc.querySelector(
+ "#useRecommendedPerformanceSettings"
+ );
+ useRecommendedPerformanceSettings.click();
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ true,
+ "pref value should be true after clicking on checkbox"
+ );
+ is(
+ Services.prefs.getIntPref("dom.ipc.processCount"),
+ DEFAULT_PROCESS_COUNT,
+ "process count should be default value"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ Services.prefs.clearUserPref("dom.ipc.processCount");
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+});
diff --git a/browser/components/preferences/tests/browser_performance_non_e10s.js b/browser/components/preferences/tests/browser_performance_non_e10s.js
new file mode 100644
index 0000000000..169afcaaa6
--- /dev/null
+++ b/browser/components/preferences/tests/browser_performance_non_e10s.js
@@ -0,0 +1,210 @@
+const DEFAULT_HW_ACCEL_PREF = Services.prefs
+ .getDefaultBranch(null)
+ .getBoolPref("layers.acceleration.disabled");
+const DEFAULT_PROCESS_COUNT = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref("dom.ipc.processCount");
+
+add_task(async function () {
+ // We must temporarily disable `Once` StaticPrefs check for the duration of
+ // this test (see bug 1556131). We must do so in a separate operation as
+ // pushPrefEnv doesn't set the preferences in the order one could expect.
+ await SpecialPowers.pushPrefEnv({
+ set: [["preferences.force-disable.check.once.policy", true]],
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF],
+ ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT],
+ ["browser.preferences.defaultPerformanceSettings.enabled", true],
+ ],
+ });
+});
+
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let useRecommendedPerformanceSettings = doc.querySelector(
+ "#useRecommendedPerformanceSettings"
+ );
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ true,
+ "pref value should be true before clicking on checkbox"
+ );
+ ok(
+ useRecommendedPerformanceSettings.checked,
+ "checkbox should be checked before clicking on checkbox"
+ );
+
+ useRecommendedPerformanceSettings.click();
+
+ let performanceSettings = doc.querySelector("#performanceSettings");
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section is shown"
+ );
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ ),
+ false,
+ "pref value should be false after clicking on checkbox"
+ );
+ ok(
+ !useRecommendedPerformanceSettings.checked,
+ "checkbox should not be checked after clicking on checkbox"
+ );
+
+ let allowHWAccel = doc.querySelector("#allowHWAccel");
+ let allowHWAccelPref = Services.prefs.getBoolPref(
+ "layers.acceleration.disabled"
+ );
+ is(
+ allowHWAccelPref,
+ DEFAULT_HW_ACCEL_PREF,
+ "pref value should be the default value before clicking on checkbox"
+ );
+ is(
+ allowHWAccel.checked,
+ !DEFAULT_HW_ACCEL_PREF,
+ "checkbox should show the invert of the default value"
+ );
+
+ let contentProcessCount = doc.querySelector("#contentProcessCount");
+ is(
+ contentProcessCount.disabled,
+ true,
+ "process count control should be disabled"
+ );
+
+ let contentProcessCountEnabledDescription = doc.querySelector(
+ "#contentProcessCountEnabledDescription"
+ );
+ is(
+ contentProcessCountEnabledDescription.hidden,
+ true,
+ "process count enabled description should be hidden"
+ );
+
+ let contentProcessCountDisabledDescription = doc.querySelector(
+ "#contentProcessCountDisabledDescription"
+ );
+ is(
+ contentProcessCountDisabledDescription.hidden,
+ false,
+ "process count enabled description should be shown"
+ );
+
+ allowHWAccel.click();
+ allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled");
+ is(
+ allowHWAccelPref,
+ !DEFAULT_HW_ACCEL_PREF,
+ "pref value should be opposite of the default value after clicking on checkbox"
+ );
+ is(
+ allowHWAccel.checked,
+ !allowHWAccelPref,
+ "checkbox should show the invert of the current value"
+ );
+
+ allowHWAccel.click();
+ allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled");
+ is(
+ allowHWAccelPref,
+ DEFAULT_HW_ACCEL_PREF,
+ "pref value should be the default value after clicking on checkbox"
+ );
+ is(
+ allowHWAccel.checked,
+ !allowHWAccelPref,
+ "checkbox should show the invert of the current value"
+ );
+
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section should be still shown"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ let performanceSettings = doc.querySelector("#performanceSettings");
+
+ is(
+ performanceSettings.hidden,
+ true,
+ "performance settings section should not be shown"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ false
+ );
+
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section should be shown"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ Services.prefs.setBoolPref("layers.acceleration.disabled", true);
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "paneGeneral", "General pane was selected");
+
+ let doc = gBrowser.contentDocument;
+
+ let performanceSettings = doc.querySelector("#performanceSettings");
+ is(
+ performanceSettings.hidden,
+ false,
+ "performance settings section should be shown"
+ );
+
+ let allowHWAccel = doc.querySelector("#allowHWAccel");
+ is(
+ Services.prefs.getBoolPref("layers.acceleration.disabled"),
+ true,
+ "pref value is false"
+ );
+ ok(!allowHWAccel.checked, "checkbox should not be checked");
+
+ Services.prefs.setBoolPref(
+ "browser.preferences.defaultPerformanceSettings.enabled",
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js b/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js
new file mode 100644
index 0000000000..829f897b72
--- /dev/null
+++ b/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml";
+
+const _checkAndOpenCookiesDialog = async doc => {
+ let cookieExceptionsButton = doc.getElementById("cookieExceptions");
+ ok(cookieExceptionsButton, "cookieExceptionsButton found");
+ let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL);
+ cookieExceptionsButton.click();
+ let dialog = await dialogPromise;
+ return dialog;
+};
+
+const _checkCookiesDialog = (dialog, buttonIds) => {
+ ok(dialog, "dialog loaded");
+ let urlLabel = dialog.document.getElementById("urlLabel");
+ ok(!urlLabel.hidden, "urlLabel should be visible");
+ let url = dialog.document.getElementById("url");
+ ok(!url.hidden, "url should be visible");
+ for (let buttonId of buttonIds) {
+ let buttonDialog = dialog.document.getElementById(buttonId);
+ ok(buttonDialog, "blockButtonDialog found");
+ is(
+ buttonDialog.hasAttribute("disabled"),
+ true,
+ "If the user hasn't added an url the button shouldn't be clickable"
+ );
+ }
+ return dialog;
+};
+
+const _addWebsiteAddressToPermissionBox = (
+ websiteAddress,
+ dialog,
+ buttonId
+) => {
+ let url = dialog.document.getElementById("url");
+ let buttonDialog = dialog.document.getElementById(buttonId);
+ url.value = websiteAddress;
+ url.dispatchEvent(new Event("input", { bubbles: true }));
+ is(
+ buttonDialog.hasAttribute("disabled"),
+ false,
+ "When the user add an url the button should be clickable"
+ );
+ buttonDialog.click();
+ let permissionsBox = dialog.document.getElementById("permissionsBox");
+ let children = permissionsBox.getElementsByAttribute("origin", "*");
+ is(!children.length, false, "Website added in url should be in the list");
+};
+
+const _checkIfPermissionsWereAdded = (dialog, expectedResult) => {
+ let permissionsBox = dialog.document.getElementById("permissionsBox");
+ for (let website of expectedResult) {
+ let elements = permissionsBox.getElementsByAttribute("origin", website);
+ is(elements.length, 1, "It should find only one coincidence");
+ }
+};
+
+const _removesAllSitesInPermissionBox = dialog => {
+ let removeAllWebsitesButton = dialog.document.getElementById(
+ "removeAllPermissions"
+ );
+ ok(removeAllWebsitesButton, "removeAllWebsitesButton found");
+ is(
+ removeAllWebsitesButton.hasAttribute("disabled"),
+ false,
+ "There should be websites in the list"
+ );
+ removeAllWebsitesButton.click();
+};
+
+add_task(async function checkCookiePermissions() {
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = win.document;
+ let buttonIds = ["btnBlock", "btnCookieSession", "btnAllow"];
+
+ let dialog = await _checkAndOpenCookiesDialog(doc);
+ _checkCookiesDialog(dialog, buttonIds);
+
+ let tests = [
+ {
+ inputWebsite: "google.com",
+ expectedResult: ["http://google.com", "https://google.com"],
+ },
+ {
+ inputWebsite: "https://google.com",
+ expectedResult: ["https://google.com"],
+ },
+ {
+ inputWebsite: "http://",
+ expectedResult: ["http://http", "https://http"],
+ },
+ {
+ inputWebsite: "s3.eu-central-1.amazonaws.com",
+ expectedResult: [
+ "http://s3.eu-central-1.amazonaws.com",
+ "https://s3.eu-central-1.amazonaws.com",
+ ],
+ },
+ {
+ inputWebsite: "file://",
+ expectedResult: ["file:///"],
+ },
+ {
+ inputWebsite: "about:config",
+ expectedResult: ["about:config"],
+ },
+ ];
+
+ for (let buttonId of buttonIds) {
+ for (let test of tests) {
+ _addWebsiteAddressToPermissionBox(test.inputWebsite, dialog, buttonId);
+ _checkIfPermissionsWereAdded(dialog, test.expectedResult);
+ _removesAllSitesInPermissionBox(dialog);
+ }
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_permissions_dialog.js b/browser/components/preferences/tests/browser_permissions_dialog.js
new file mode 100644
index 0000000000..3a5e0f95c2
--- /dev/null
+++ b/browser/components/preferences/tests/browser_permissions_dialog.js
@@ -0,0 +1,642 @@
+"use strict";
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml";
+const URL = "http://www.example.com";
+const URI = Services.io.newURI(URL);
+var sitePermissionsDialog;
+let settingsButtonMap = {
+ "desktop-notification": "notificationSettingsButton",
+ speaker: "speakerSettingsButton",
+};
+
+function checkMenulistPermissionItem(origin, state) {
+ let doc = sitePermissionsDialog.document;
+
+ let label = doc.getElementsByTagName("label")[3];
+ Assert.equal(label.value, origin);
+
+ let menulist = doc.getElementsByTagName("menulist")[0];
+ Assert.equal(menulist.value, state);
+}
+
+async function openPermissionsDialog(permissionType) {
+ let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [settingsButtonMap[permissionType]],
+ function (settingsButtonId) {
+ let doc = content.document;
+ let settingsButton = doc.getElementById(settingsButtonId);
+ settingsButton.click();
+ }
+ );
+
+ sitePermissionsDialog = await dialogOpened;
+ await sitePermissionsDialog.document.mozSubdialogReady;
+}
+
+add_task(async function openSitePermissionsDialog() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await openPermissionsDialog("desktop-notification");
+});
+
+add_task(async function addPermission() {
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ // First item in the richlistbox contains column headers.
+ Assert.equal(
+ richlistbox.itemCount,
+ 0,
+ "Number of permission items is 0 initially"
+ );
+
+ // Add notification permission for a website.
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Observe the added permission changes in the dialog UI.
+ Assert.equal(richlistbox.itemCount, 1);
+ checkMenulistPermissionItem(URL, Services.perms.ALLOW_ACTION);
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+});
+
+add_task(async function addPermissionPrivateBrowsing() {
+ let privateBrowsingPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(URI, {
+ privateBrowsingId: 1,
+ });
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ Assert.equal(
+ richlistbox.itemCount,
+ 0,
+ "Number of permission items is 0 initially"
+ );
+
+ // Add a session permission for private browsing.
+ PermissionTestUtils.add(
+ privateBrowsingPrincipal,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_SESSION
+ );
+
+ // The permission should not show in the dialog UI.
+ Assert.equal(richlistbox.itemCount, 0);
+
+ PermissionTestUtils.remove(privateBrowsingPrincipal, "desktop-notification");
+
+ // Add a permanent permission for private browsing
+ // The permission manager will store it as EXPIRE_SESSION
+ PermissionTestUtils.add(
+ privateBrowsingPrincipal,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // The permission should not show in the dialog UI.
+ Assert.equal(richlistbox.itemCount, 0);
+
+ PermissionTestUtils.remove(privateBrowsingPrincipal, "desktop-notification");
+});
+
+add_task(async function observePermissionChange() {
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Change the permission.
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.DENY_ACTION
+ );
+
+ checkMenulistPermissionItem(URL, Services.perms.DENY_ACTION);
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+});
+
+add_task(async function observePermissionDelete() {
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ Assert.equal(
+ richlistbox.itemCount,
+ 1,
+ "The box contains one permission item initially"
+ );
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+
+ Assert.equal(richlistbox.itemCount, 0);
+});
+
+add_task(async function onPermissionChange() {
+ let doc = sitePermissionsDialog.document;
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Change the permission state in the UI.
+ doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click();
+
+ Assert.equal(
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+ .capability,
+ Services.perms.ALLOW_ACTION,
+ "Permission state does not change before saving changes"
+ );
+
+ doc.querySelector("dialog").getButton("accept").click();
+
+ await TestUtils.waitForCondition(
+ () =>
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+ .capability == Services.perms.DENY_ACTION
+ );
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+});
+
+add_task(async function onPermissionDelete() {
+ await openPermissionsDialog("desktop-notification");
+
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ richlistbox.selectItem(richlistbox.getItemAtIndex(0));
+ doc.getElementById("removePermission").click();
+
+ await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
+
+ Assert.equal(
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+ .capability,
+ Services.perms.ALLOW_ACTION,
+ "Permission is not deleted before saving changes"
+ );
+
+ doc.querySelector("dialog").getButton("accept").click();
+
+ await TestUtils.waitForCondition(
+ () =>
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification") ==
+ null
+ );
+});
+
+add_task(async function onAllPermissionsDelete() {
+ await openPermissionsDialog("desktop-notification");
+
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+ let u = Services.io.newURI("http://www.test.com");
+ PermissionTestUtils.add(
+ u,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ doc.getElementById("removeAllPermissions").click();
+ await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
+
+ Assert.equal(
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+ .capability,
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ PermissionTestUtils.getPermissionObject(u, "desktop-notification")
+ .capability,
+ Services.perms.ALLOW_ACTION,
+ "Permissions are not deleted before saving changes"
+ );
+
+ doc.querySelector("dialog").getButton("accept").click();
+
+ await TestUtils.waitForCondition(
+ () =>
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification") ==
+ null &&
+ PermissionTestUtils.getPermissionObject(u, "desktop-notification") == null
+ );
+});
+
+add_task(async function onPermissionChangeAndDelete() {
+ await openPermissionsDialog("desktop-notification");
+
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Change the permission state in the UI.
+ doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click();
+
+ // Remove that permission by clicking the "Remove" button.
+ richlistbox.selectItem(richlistbox.getItemAtIndex(0));
+ doc.getElementById("removePermission").click();
+
+ await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
+
+ doc.querySelector("dialog").getButton("accept").click();
+
+ await TestUtils.waitForCondition(
+ () =>
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification") ==
+ null
+ );
+});
+
+add_task(async function onPermissionChangeCancel() {
+ await openPermissionsDialog("desktop-notification");
+
+ let doc = sitePermissionsDialog.document;
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Change the permission state in the UI.
+ doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click();
+
+ doc.querySelector("dialog").getButton("cancel").click();
+
+ Assert.equal(
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+ .capability,
+ Services.perms.ALLOW_ACTION,
+ "Permission state does not change on clicking cancel"
+ );
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+});
+
+add_task(async function onPermissionDeleteCancel() {
+ await openPermissionsDialog("desktop-notification");
+
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Remove that permission by clicking the "Remove" button.
+ richlistbox.selectItem(richlistbox.getItemAtIndex(0));
+ doc.getElementById("removePermission").click();
+
+ await TestUtils.waitForCondition(() => richlistbox.itemCount == 0);
+
+ doc.querySelector("dialog").getButton("cancel").click();
+
+ Assert.equal(
+ PermissionTestUtils.getPermissionObject(URI, "desktop-notification")
+ .capability,
+ Services.perms.ALLOW_ACTION,
+ "Permission state does not change on clicking cancel"
+ );
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+});
+
+add_task(async function onSearch() {
+ await openPermissionsDialog("desktop-notification");
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+ let searchBox = doc.getElementById("searchBox");
+
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+ searchBox.value = "www.example.com";
+
+ let u = Services.io.newURI("http://www.test.com");
+ PermissionTestUtils.add(
+ u,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ Assert.equal(
+ doc.getElementsByAttribute("origin", "http://www.test.com")[0],
+ null
+ );
+ Assert.equal(
+ doc.getElementsByAttribute("origin", "http://www.example.com")[0],
+ richlistbox.getItemAtIndex(0)
+ );
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+ PermissionTestUtils.remove(u, "desktop-notification");
+
+ doc.querySelector("dialog").getButton("cancel").click();
+});
+
+add_task(async function onPermissionsSort() {
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+ let u = Services.io.newURI("http://www.test.com");
+ PermissionTestUtils.add(
+ u,
+ "desktop-notification",
+ Services.perms.DENY_ACTION
+ );
+
+ await openPermissionsDialog("desktop-notification");
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ // Test default arrangement(Allow followed by Block).
+ Assert.equal(
+ richlistbox.getItemAtIndex(0).getAttribute("origin"),
+ "http://www.example.com"
+ );
+ Assert.equal(
+ richlistbox.getItemAtIndex(1).getAttribute("origin"),
+ "http://www.test.com"
+ );
+
+ doc.getElementById("statusCol").click();
+
+ // Test the rearrangement(Block followed by Allow).
+ Assert.equal(
+ richlistbox.getItemAtIndex(0).getAttribute("origin"),
+ "http://www.test.com"
+ );
+ Assert.equal(
+ richlistbox.getItemAtIndex(1).getAttribute("origin"),
+ "http://www.example.com"
+ );
+
+ doc.getElementById("siteCol").click();
+
+ // Test the rearrangement(Website names arranged in alphabhetical order).
+ Assert.equal(
+ richlistbox.getItemAtIndex(0).getAttribute("origin"),
+ "http://www.example.com"
+ );
+ Assert.equal(
+ richlistbox.getItemAtIndex(1).getAttribute("origin"),
+ "http://www.test.com"
+ );
+
+ doc.getElementById("siteCol").click();
+
+ // Test the rearrangement(Website names arranged in reverse alphabhetical order).
+ Assert.equal(
+ richlistbox.getItemAtIndex(0).getAttribute("origin"),
+ "http://www.test.com"
+ );
+ Assert.equal(
+ richlistbox.getItemAtIndex(1).getAttribute("origin"),
+ "http://www.example.com"
+ );
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+ PermissionTestUtils.remove(u, "desktop-notification");
+
+ doc.querySelector("dialog").getButton("cancel").click();
+});
+
+add_task(async function onPermissionDisable() {
+ // Enable desktop-notification permission prompts.
+ Services.prefs.setIntPref(
+ "permissions.default.desktop-notification",
+ SitePermissions.UNKNOWN
+ );
+
+ await openPermissionsDialog("desktop-notification");
+ let doc = sitePermissionsDialog.document;
+
+ // Check if the enabled state is reflected in the checkbox.
+ let checkbox = doc.getElementById("permissionsDisableCheckbox");
+ Assert.equal(checkbox.checked, false);
+
+ // Disable permission and click on "Cancel".
+ checkbox.checked = true;
+ doc.querySelector("dialog").getButton("cancel").click();
+
+ // Check that the permission is not disabled yet.
+ let perm = Services.prefs.getIntPref(
+ "permissions.default.desktop-notification"
+ );
+ Assert.equal(perm, SitePermissions.UNKNOWN);
+
+ // Open the dialog once again.
+ await openPermissionsDialog("desktop-notification");
+ doc = sitePermissionsDialog.document;
+
+ // Disable permission and save changes.
+ checkbox = doc.getElementById("permissionsDisableCheckbox");
+ checkbox.checked = true;
+ doc.querySelector("dialog").getButton("accept").click();
+
+ // Check if the permission is now disabled.
+ perm = Services.prefs.getIntPref("permissions.default.desktop-notification");
+ Assert.equal(perm, SitePermissions.BLOCK);
+
+ // Open the dialog once again and check if the disabled state is still reflected in the checkbox.
+ await openPermissionsDialog("desktop-notification");
+ doc = sitePermissionsDialog.document;
+ checkbox = doc.getElementById("permissionsDisableCheckbox");
+ Assert.equal(checkbox.checked, true);
+
+ // Close the dialog and clean up.
+ doc.querySelector("dialog").getButton("cancel").click();
+ Services.prefs.setIntPref(
+ "permissions.default.desktop-notification",
+ SitePermissions.UNKNOWN
+ );
+});
+
+add_task(async function checkDefaultPermissionState() {
+ // Set default permission state to ALLOW.
+ Services.prefs.setIntPref(
+ "permissions.default.desktop-notification",
+ SitePermissions.ALLOW
+ );
+
+ await openPermissionsDialog("desktop-notification");
+ let doc = sitePermissionsDialog.document;
+
+ // Check if the enabled state is reflected in the checkbox.
+ let checkbox = doc.getElementById("permissionsDisableCheckbox");
+ Assert.equal(checkbox.checked, false);
+
+ // Check the checkbox and then uncheck it.
+ checkbox.checked = true;
+ checkbox.checked = false;
+
+ // Save changes.
+ doc.querySelector("dialog").getButton("accept").click();
+
+ // Check if the default permission state is retained (and not automatically set to SitePermissions.UNKNOWN).
+ let state = Services.prefs.getIntPref(
+ "permissions.default.desktop-notification"
+ );
+ Assert.equal(state, SitePermissions.ALLOW);
+
+ // Clean up.
+ Services.prefs.setIntPref(
+ "permissions.default.desktop-notification",
+ SitePermissions.UNKNOWN
+ );
+});
+
+add_task(async function testTabBehaviour() {
+ // Test tab behaviour inside the permissions setting dialog when site permissions are selected.
+ // Only selected items in the richlistbox should be tabable for accessibility reasons.
+
+ // Force tabfocus for all elements on OSX.
+ SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+
+ PermissionTestUtils.add(
+ URI,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+ let u = Services.io.newURI("http://www.test.com");
+ PermissionTestUtils.add(
+ u,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await openPermissionsDialog("desktop-notification");
+ let doc = sitePermissionsDialog.document;
+
+ EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog);
+ let richlistbox = doc.getElementById("permissionsBox");
+ is(
+ richlistbox,
+ doc.activeElement.closest("#permissionsBox"),
+ "The richlistbox is focused after pressing tab once."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, sitePermissionsDialog);
+ EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog);
+ let menulist = doc
+ .getElementById("permissionsBox")
+ .itemChildren[1].getElementsByTagName("menulist")[0];
+ is(
+ menulist,
+ doc.activeElement,
+ "The menulist inside the selected richlistitem is focused now"
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog);
+ let removeButton = doc.getElementById("removePermission");
+ is(
+ removeButton,
+ doc.activeElement,
+ "The focus moves outside the richlistbox and onto the remove button"
+ );
+
+ PermissionTestUtils.remove(URI, "desktop-notification");
+ PermissionTestUtils.remove(u, "desktop-notification");
+
+ doc.querySelector("dialog").getButton("cancel").click();
+});
+
+add_task(async function addSpeakerPermission() {
+ let enabled = Services.prefs.getBoolPref("media.setsinkid.enabled", false);
+ let speakerRow =
+ gBrowser.contentDocument.getElementById("speakerSettingsRow");
+ Assert.equal(
+ BrowserTestUtils.is_visible(speakerRow),
+ enabled,
+ "speakerRow visible"
+ );
+ if (!enabled) {
+ return;
+ }
+
+ await openPermissionsDialog("speaker");
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+ Assert.equal(
+ richlistbox.itemCount,
+ 0,
+ "Number of permission items is 0 initially"
+ );
+ // Add an allow permission for a device.
+ let deviceId = "DEVICE-ID";
+ let devicePermissionId = `speaker^${deviceId}`;
+ PermissionTestUtils.add(URI, devicePermissionId, Services.perms.ALLOW_ACTION);
+
+ // Observe the added permission changes in the dialog UI.
+ Assert.equal(richlistbox.itemCount, 1, "itemCount with allow");
+ checkMenulistPermissionItem(URL, Services.perms.ALLOW_ACTION);
+
+ // Check that an all-device deny permission overrides the device-specific
+ // allow permission.
+ PermissionTestUtils.add(URI, "speaker", Services.perms.DENY_ACTION);
+
+ Assert.equal(richlistbox.itemCount, 1, "itemCount with deny and allow");
+ let richlistitem = richlistbox.itemChildren[0];
+ let siteStatus = richlistitem.querySelector(".website-status");
+ Assert.equal(
+ siteStatus.value,
+ Services.perms.DENY_ACTION,
+ "website status with deny and allow"
+ );
+ // The website status element is not a menulist because all-device allow is
+ // not an option.
+ Assert.equal(siteStatus.tagName, "hbox");
+ Assert.equal(siteStatus.firstElementChild.tagName, "label");
+
+ PermissionTestUtils.remove(URI, devicePermissionId);
+ PermissionTestUtils.remove(URI, "speaker");
+
+ doc.querySelector("dialog").getButton("cancel").click();
+});
+
+add_task(async function removeTab() {
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js b/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js
new file mode 100644
index 0000000000..37bde1a275
--- /dev/null
+++ b/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js
@@ -0,0 +1,145 @@
+"use strict";
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml";
+
+let sitePermissionsDialog;
+
+let principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("http://www.example.com"),
+ {}
+);
+let pbPrincipal = Services.scriptSecurityManager.principalWithOA(principal, {
+ privateBrowsingId: 1,
+});
+let principalB = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://example.org"),
+ {}
+);
+
+/**
+ * Replaces the default permissions defined in browser/app/permissions with our
+ * own test-only permissions and instructs the permission manager to import
+ * them.
+ */
+async function addDefaultTestPermissions() {
+ // create a file in the temp directory with the defaults.
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("test_default_permissions");
+
+ await IOUtils.writeUTF8(
+ file.path,
+ `origin\tinstall\t1\t${principal.origin}\norigin\tinstall\t1\t${pbPrincipal.origin}\n`
+ );
+
+ // Change the default permission file path.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["permissions.manager.defaultsUrl", Services.io.newFileURI(file).spec],
+ ],
+ });
+
+ // Call the permission manager to reload default permissions from file.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ registerCleanupFunction(async () => {
+ // Clean up temporary default permission file.
+ await IOUtils.remove(file.path);
+
+ // Restore non-test default permissions.
+ await SpecialPowers.popPrefEnv();
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+ });
+}
+
+async function openPermissionsDialog() {
+ let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let doc = content.document;
+ let settingsButton = doc.getElementById("addonExceptions");
+ settingsButton.click();
+ });
+
+ sitePermissionsDialog = await dialogOpened;
+ await sitePermissionsDialog.document.mozSubdialogReady;
+}
+
+add_setup(async function () {
+ await addDefaultTestPermissions();
+});
+
+/**
+ * Tests that default (persistent) private browsing permissions can be removed.
+ */
+add_task(async function removeAll() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await openPermissionsDialog();
+
+ let doc = sitePermissionsDialog.document;
+ let richlistbox = doc.getElementById("permissionsBox");
+
+ // First item in the richlistbox contains column headers.
+ Assert.equal(
+ richlistbox.itemCount,
+ 2,
+ "Should have the two default permission entries initially."
+ );
+
+ info("Adding a new non-default install permission");
+ PermissionTestUtils.add(principalB, "install", Services.perms.ALLOW_ACTION);
+
+ info("Waiting for the permission to appear in the list.");
+ await BrowserTestUtils.waitForMutationCondition(
+ richlistbox,
+ { childList: true },
+ () => richlistbox.itemCount == 3
+ );
+
+ info("Clicking remove all.");
+ doc.getElementById("removeAllPermissions").click();
+
+ info("Waiting for all list items to be cleared.");
+ await BrowserTestUtils.waitForMutationCondition(
+ richlistbox,
+ { childList: true },
+ () => richlistbox.itemCount == 0
+ );
+
+ let dialogClosePromise = BrowserTestUtils.waitForEvent(
+ sitePermissionsDialog,
+ "dialogclosing",
+ true
+ );
+
+ info("Accepting dialog to apply the changes.");
+ doc.querySelector("dialog").getButton("accept").click();
+
+ info("Waiting for dialog to close.");
+ await dialogClosePromise;
+
+ info("Waiting for all permissions to be removed.");
+ await TestUtils.waitForCondition(
+ () =>
+ PermissionTestUtils.getPermissionObject(principal, "install") == null &&
+ PermissionTestUtils.getPermissionObject(pbPrincipal, "install") == null &&
+ PermissionTestUtils.getPermissionObject(principalB, "install") == null
+ );
+
+ info("Opening the permissions dialog again.");
+ await openPermissionsDialog();
+
+ Assert.equal(
+ richlistbox.itemCount,
+ 0,
+ "Permission list should still be empty."
+ );
+
+ // Cleanup
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.perms.removeAll();
+});
diff --git a/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js b/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js
new file mode 100644
index 0000000000..537ee3db72
--- /dev/null
+++ b/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js
@@ -0,0 +1,38 @@
+"use strict";
+
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml";
+
+add_task(async function urlFieldVisibleForPopupPermissions(finish) {
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = win.document;
+ let popupPolicyCheckbox = doc.getElementById("popupPolicy");
+ ok(
+ !popupPolicyCheckbox.checked,
+ "popupPolicyCheckbox should be unchecked by default"
+ );
+ popupPolicyCheckbox.click();
+ let popupPolicyButton = doc.getElementById("popupPolicyButton");
+ ok(popupPolicyButton, "popupPolicyButton found");
+ let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL);
+ popupPolicyButton.click();
+ let dialog = await dialogPromise;
+ ok(dialog, "dialog loaded");
+
+ let urlLabel = dialog.document.getElementById("urlLabel");
+ ok(
+ !urlLabel.hidden,
+ "urlLabel should be visible when one of block/session/allow visible"
+ );
+ let url = dialog.document.getElementById("url");
+ ok(
+ !url.hidden,
+ "url should be visible when one of block/session/allow visible"
+ );
+
+ popupPolicyCheckbox.click();
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_primaryPassword.js b/browser/components/preferences/tests/browser_primaryPassword.js
new file mode 100644
index 0000000000..4de28a1fdb
--- /dev/null
+++ b/browser/components/preferences/tests/browser_primaryPassword.js
@@ -0,0 +1,130 @@
+const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
+);
+const { OSKeyStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/OSKeyStore.sys.mjs"
+);
+
+add_task(async function () {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected");
+
+ let doc = gBrowser.contentDocument;
+ // Fake the subdialog and LoginHelper
+ let win = doc.defaultView;
+ let dialogURL = "";
+ let dialogOpened = false;
+ XPCOMUtils.defineLazyGetter(win, "gSubDialog", () => ({
+ open(aDialogURL, { closingCallback: aCallback }) {
+ dialogOpened = true;
+ dialogURL = aDialogURL;
+ primaryPasswordSet = primaryPasswordNextState;
+ aCallback();
+ },
+ }));
+
+ let primaryPasswordSet = false;
+ win.LoginHelper = {
+ isPrimaryPasswordSet() {
+ return primaryPasswordSet;
+ },
+ };
+
+ let checkbox = doc.querySelector("#useMasterPassword");
+ checkbox.scrollIntoView();
+ ok(
+ !checkbox.checked,
+ "primary password checkbox should be unchecked by default"
+ );
+ let button = doc.getElementById("changeMasterPassword");
+ ok(button.disabled, "primary password button should be disabled by default");
+
+ let primaryPasswordNextState = false;
+ if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin() && OSKeyStore.canReauth()) {
+ let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
+ checkbox.click();
+ info("waiting for os auth dialog to appear and get canceled");
+ await osAuthDialogShown;
+ await TestUtils.waitForCondition(
+ () => !checkbox.checked,
+ "wait for checkbox to get unchecked"
+ );
+ ok(!dialogOpened, "the dialog should not have opened");
+ ok(
+ !dialogURL,
+ "the changemp dialog should not have been opened when the os auth dialog is canceled"
+ );
+ ok(
+ !checkbox.checked,
+ "primary password checkbox should be unchecked after canceling os auth dialog"
+ );
+ ok(button.disabled, "button should be disabled after canceling os auth");
+ }
+
+ primaryPasswordNextState = true;
+ if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin() && OSKeyStore.canReauth()) {
+ let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ checkbox.click();
+ info("waiting for os auth dialog to appear");
+ await osAuthDialogShown;
+ info("waiting for dialogURL to get set");
+ await TestUtils.waitForCondition(
+ () => dialogURL,
+ "wait for open to get called asynchronously"
+ );
+ is(
+ dialogURL,
+ "chrome://mozapps/content/preferences/changemp.xhtml",
+ "clicking on the checkbox should open the primary password dialog"
+ );
+ } else {
+ primaryPasswordSet = true;
+ doc.defaultView.gPrivacyPane._initMasterPasswordUI();
+ await TestUtils.waitForCondition(
+ () => !button.disabled,
+ "waiting for primary password button to get enabled"
+ );
+ }
+ ok(!button.disabled, "primary password button should now be enabled");
+ ok(checkbox.checked, "primary password checkbox should be checked now");
+
+ dialogURL = "";
+ button.doCommand();
+ await TestUtils.waitForCondition(
+ () => dialogURL,
+ "wait for open to get called asynchronously"
+ );
+ is(
+ dialogURL,
+ "chrome://mozapps/content/preferences/changemp.xhtml",
+ "clicking on the button should open the primary password dialog"
+ );
+ ok(!button.disabled, "primary password button should still be enabled");
+ ok(checkbox.checked, "primary password checkbox should be checked still");
+
+ // Confirm that we won't automatically respond to the dialog,
+ // since we don't expect a dialog here, we want the test to fail if one appears.
+ is(
+ Services.prefs.getStringPref(
+ "toolkit.osKeyStore.unofficialBuildOnlyLogin",
+ ""
+ ),
+ "",
+ "Pref should be set to an empty string"
+ );
+
+ primaryPasswordNextState = false;
+ dialogURL = "";
+ checkbox.click();
+ is(
+ dialogURL,
+ "chrome://mozapps/content/preferences/removemp.xhtml",
+ "clicking on the checkbox to uncheck primary password should show the removal dialog"
+ );
+ ok(button.disabled, "primary password button should now be disabled");
+ ok(!checkbox.checked, "primary password checkbox should now be unchecked");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js b/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js
new file mode 100644
index 0000000000..722fe9a215
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js
@@ -0,0 +1,210 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file tests the Privacy pane's Cookie Banner Handling UI.
+
+"use strict";
+
+const FEATURE_PREF = "cookiebanners.ui.desktop.enabled";
+const MODE_PREF = "cookiebanners.service.mode";
+const PBM_MODE_PREF = "cookiebanners.service.mode.privateBrowsing";
+const DETECT_ONLY_PREF = "cookiebanners.service.detectOnly";
+
+const GROUPBOX_ID = "cookieBannerHandlingGroup";
+const CHECKBOX_ID = "handleCookieBanners";
+
+// Test the section is hidden on page load if the feature pref is disabled.
+add_task(async function test_section_hidden_when_feature_flag_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FEATURE_PREF, false],
+ [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#privacy" },
+ async function (browser) {
+ let groupbox = browser.contentDocument.getElementById(GROUPBOX_ID);
+ is_element_hidden(groupbox, "#cookieBannerHandlingGroup is hidden");
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test the section is shown on page load if the feature pref is enabled.
+add_task(async function test_section_shown_when_feature_flag_enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FEATURE_PREF, true],
+ [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#privacy" },
+ async function (browser) {
+ let groupbox = browser.contentDocument.getElementById(GROUPBOX_ID);
+ is_element_visible(groupbox, "#cookieBannerHandlingGroup is visible");
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test the checkbox is unchecked in DISABLED mode.
+add_task(async function test_checkbox_unchecked_disabled_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FEATURE_PREF, true],
+ [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#privacy" },
+ async function (browser) {
+ let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID);
+ ok(!checkbox.checked, "checkbox is not checked in DISABLED mode");
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test the checkbox is unchecked in detect-only mode.
+add_task(async function test_checkbox_unchecked_detect_only_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FEATURE_PREF, true],
+ [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT],
+ [DETECT_ONLY_PREF, true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#privacy" },
+ async function (browser) {
+ let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID);
+ ok(!checkbox.checked, "checkbox is not checked in detect-only mode");
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test the checkbox is checked in REJECT_OR_ACCEPT mode.
+add_task(async function test_checkbox_checked_reject_or_accept_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FEATURE_PREF, true],
+ [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#privacy" },
+ async function (browser) {
+ let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID);
+ ok(checkbox.checked, "checkbox is checked in REJECT_OR_ACCEPT mode");
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test the checkbox is checked in REJECT mode.
+add_task(async function test_checkbox_checked_reject_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FEATURE_PREF, true],
+ [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#privacy" },
+ async function (browser) {
+ let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID);
+ ok(checkbox.checked, "checkbox is checked in REJECT mode");
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test that toggling the checkbox toggles the mode pref value as expected,
+// and also disables detect only mode, as expected.
+add_task(async function test_checkbox_modifies_prefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FEATURE_PREF, true],
+ [MODE_PREF, Ci.nsICookieBannerService.MODE_UNSET],
+ [PBM_MODE_PREF, Ci.nsICookieBannerService.MODE_UNSET],
+ [DETECT_ONLY_PREF, true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:preferences#privacy" },
+ async function (browser) {
+ let checkboxSelector = "#" + CHECKBOX_ID;
+ let checkbox = browser.contentDocument.querySelector(checkboxSelector);
+ let section = browser.contentDocument.getElementById(GROUPBOX_ID);
+
+ section.scrollIntoView();
+
+ Assert.ok(
+ !checkbox.checked,
+ "initially, the checkbox should be unchecked"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ checkboxSelector,
+ {},
+ browser
+ );
+ Assert.ok(checkbox.checked, "checkbox should be checked");
+ Assert.equal(
+ Ci.nsICookieBannerService.MODE_REJECT,
+ Services.prefs.getIntPref(MODE_PREF),
+ "cookie banner handling mode should be set to REJECT mode after checking the checkbox"
+ );
+ Assert.equal(
+ Ci.nsICookieBannerService.MODE_REJECT,
+ Services.prefs.getIntPref(PBM_MODE_PREF),
+ "cookie banner handling mode for PBM should be set to REJECT mode after checking the checkbox"
+ );
+ Assert.equal(
+ false,
+ Services.prefs.getBoolPref(DETECT_ONLY_PREF),
+ "cookie banner handling detect-only mode should be disabled after checking the checkbox"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ checkboxSelector,
+ {},
+ browser
+ );
+ Assert.ok(!checkbox.checked, "checkbox should be unchecked");
+ Assert.equal(
+ Ci.nsICookieBannerService.MODE_DISABLED,
+ Services.prefs.getIntPref(MODE_PREF),
+ "cookie banner handling mode should be set to DISABLED mode after unchecking the checkbox"
+ );
+ Assert.equal(
+ Ci.nsICookieBannerService.MODE_DISABLED,
+ Services.prefs.getIntPref(PBM_MODE_PREF),
+ "cookie banner handling mode for PBM should be set to DISABLED mode after unchecking the checkbox"
+ );
+ Assert.equal(
+ false,
+ Services.prefs.getBoolPref(DETECT_ONLY_PREF),
+ "cookie banner handling detect-only mode should still be disabled after unchecking the checkbox"
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js
new file mode 100644
index 0000000000..48469cfce4
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js
@@ -0,0 +1,844 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(4);
+
+const { EnterprisePolicyTesting, PoliciesPrefTracker } =
+ ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+
+ChromeUtils.defineESModuleGetters(this, {
+ DoHConfigController: "resource:///modules/DoHConfig.sys.mjs",
+ DoHController: "resource:///modules/DoHController.sys.mjs",
+ DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs",
+});
+
+const TRR_MODE_PREF = "network.trr.mode";
+const TRR_URI_PREF = "network.trr.uri";
+const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri";
+const ROLLOUT_ENABLED_PREF = "doh-rollout.enabled";
+const ROLLOUT_SELF_ENABLED_PREF = "doh-rollout.self-enabled";
+const HEURISTICS_DISABLED_PREF = "doh-rollout.disable-heuristics";
+const FIRST_RESOLVER_VALUE = DoHTestUtils.providers[0].uri;
+const SECOND_RESOLVER_VALUE = DoHTestUtils.providers[1].uri;
+const DEFAULT_RESOLVER_VALUE = FIRST_RESOLVER_VALUE;
+
+const defaultPrefValues = Object.freeze({
+ [TRR_MODE_PREF]: 0,
+ [TRR_CUSTOM_URI_PREF]: "",
+});
+
+// See bug 1741554. This test should not actually try to create a connection to
+// the real DoH endpoint. But a background request could do that while the test
+// is in progress, before we've actually disabled TRR, and would cause a crash
+// due to connecting to a non-local IP.
+// To prevent that we override the IP to a local address.
+Cc["@mozilla.org/network/native-dns-override;1"]
+ .getService(Ci.nsINativeDNSResolverOverride)
+ .addIPOverride("mozilla.cloudflare-dns.com", "127.0.0.1");
+
+async function clearEvents() {
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_ALL_CHANNELS,
+ true
+ ).parent;
+ return !events || !events.length;
+ });
+}
+
+async function getEvent(filter1, filter2) {
+ let event = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_ALL_CHANNELS,
+ true
+ ).parent;
+ return events?.find(e => e[1] == filter1 && e[2] == filter2);
+ }, "recorded telemetry for the load");
+ event.shift();
+ return event;
+}
+
+async function resetPrefs() {
+ await DoHTestUtils.resetRemoteSettingsConfig();
+ await DoHController._uninit();
+ Services.prefs.clearUserPref(TRR_MODE_PREF);
+ Services.prefs.clearUserPref(TRR_URI_PREF);
+ Services.prefs.clearUserPref(TRR_CUSTOM_URI_PREF);
+ Services.prefs.getChildList("doh-rollout.").forEach(pref => {
+ Services.prefs.clearUserPref(pref);
+ });
+ // Clear out any telemetry events generated by DoHController so that we don't
+ // confuse tests running after this one that are looking at those.
+ Services.telemetry.clearEvents();
+ await DoHController.init();
+}
+Services.prefs.setStringPref("network.trr.confirmationNS", "skip");
+
+registerCleanupFunction(async () => {
+ await resetPrefs();
+ Services.prefs.clearUserPref("network.trr.confirmationNS");
+});
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["toolkit.telemetry.testing.overrideProductsCheck", true]],
+ });
+
+ await DoHTestUtils.resetRemoteSettingsConfig();
+});
+
+function waitForPrefObserver(name) {
+ return new Promise(resolve => {
+ const observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aData == name) {
+ Services.prefs.removeObserver(name, observer);
+ resolve();
+ }
+ },
+ };
+ Services.prefs.addObserver(name, observer);
+ });
+}
+
+async function testWithProperties(props, startTime) {
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: testing with " +
+ JSON.stringify(props)
+ );
+
+ // There are two different signals that the DoHController is ready, depending
+ // on the config being tested. If we're setting the TRR mode pref, we should
+ // expect the disable-heuristics pref to be set as the signal. Else, we can
+ // expect the self-enabled pref as the signal.
+ let rolloutReadyPromise;
+ if (props.hasOwnProperty(TRR_MODE_PREF)) {
+ if (
+ [2, 3, 5].includes(props[TRR_MODE_PREF]) &&
+ props.hasOwnProperty(ROLLOUT_ENABLED_PREF)
+ ) {
+ // Only initialize the promise if we're going to enable the rollout -
+ // otherwise we will never await it, which could cause a leak if it doesn't
+ // end up resolving.
+ rolloutReadyPromise = waitForPrefObserver(HEURISTICS_DISABLED_PREF);
+ }
+ Services.prefs.setIntPref(TRR_MODE_PREF, props[TRR_MODE_PREF]);
+ }
+ if (props.hasOwnProperty(ROLLOUT_ENABLED_PREF)) {
+ if (!rolloutReadyPromise) {
+ rolloutReadyPromise = waitForPrefObserver(ROLLOUT_SELF_ENABLED_PREF);
+ }
+ Services.prefs.setBoolPref(
+ ROLLOUT_ENABLED_PREF,
+ props[ROLLOUT_ENABLED_PREF]
+ );
+ await rolloutReadyPromise;
+ }
+ if (props.hasOwnProperty(TRR_CUSTOM_URI_PREF)) {
+ Services.prefs.setStringPref(
+ TRR_CUSTOM_URI_PREF,
+ props[TRR_CUSTOM_URI_PREF]
+ );
+ }
+ if (props.hasOwnProperty(TRR_URI_PREF)) {
+ Services.prefs.setStringPref(TRR_URI_PREF, props[TRR_URI_PREF]);
+ }
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ info(Date.now() - startTime + ": testWithProperties: tab now open");
+ let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup");
+ let uriTextbox = doc.getElementById("dohEnabledInputField");
+ let resolverMenulist = doc.getElementById("dohStrictResolverChoices");
+ let modePrefChangedPromise;
+ let uriPrefChangedPromise;
+ let disableHeuristicsPrefChangedPromise;
+
+ modeRadioGroup.scrollIntoView();
+
+ if (props.hasOwnProperty("expectedSelectedIndex")) {
+ await TestUtils.waitForCondition(
+ () => modeRadioGroup.selectedIndex === props.expectedSelectedIndex
+ );
+ is(
+ modeRadioGroup.selectedIndex,
+ props.expectedSelectedIndex,
+ "dohCategoryRadioGroup has expected selected index"
+ );
+ }
+ if (props.hasOwnProperty("expectedUriValue")) {
+ await TestUtils.waitForCondition(
+ () => uriTextbox.value === props.expectedUriValue
+ );
+ is(
+ uriTextbox.value,
+ props.expectedUriValue,
+ "URI textbox has expected value"
+ );
+ }
+ if (props.hasOwnProperty("expectedResolverListValue")) {
+ await TestUtils.waitForCondition(
+ () => resolverMenulist.value === props.expectedResolverListValue
+ );
+ is(
+ resolverMenulist.value,
+ props.expectedResolverListValue,
+ "resolver menulist has expected value"
+ );
+ }
+
+ if (props.clickMode) {
+ await clearEvents();
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: clickMode, waiting for the pref observer"
+ );
+ modePrefChangedPromise = waitForPrefObserver(TRR_MODE_PREF);
+ if (props.hasOwnProperty("expectedDisabledHeuristics")) {
+ disableHeuristicsPrefChangedPromise = waitForPrefObserver(
+ HEURISTICS_DISABLED_PREF
+ );
+ }
+ info(
+ Date.now() - startTime + ": testWithProperties: clickMode, pref changed"
+ );
+ let option = doc.getElementById(props.clickMode);
+ option.scrollIntoView();
+ let win = doc.ownerGlobal;
+ EventUtils.synthesizeMouseAtCenter(option, {}, win);
+ info(
+ `${Date.now() - startTime} : testWithProperties: clickMode=${
+ props.clickMode
+ }, mouse click synthesized`
+ );
+ let clickEvent = await getEvent("security.doh.settings", "mode_changed");
+ Assert.deepEqual(clickEvent, [
+ "security.doh.settings",
+ "mode_changed",
+ "button",
+ props.clickMode,
+ ]);
+ }
+ if (props.hasOwnProperty("selectResolver")) {
+ await clearEvents();
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: selectResolver, creating change event"
+ );
+ resolverMenulist.focus();
+ resolverMenulist.value = props.selectResolver;
+ resolverMenulist.dispatchEvent(new Event("input", { bubbles: true }));
+ resolverMenulist.dispatchEvent(new Event("command", { bubbles: true }));
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: selectResolver, item value set and events dispatched"
+ );
+ let choiceEvent = await getEvent(
+ "security.doh.settings",
+ "provider_choice"
+ );
+ Assert.deepEqual(choiceEvent, [
+ "security.doh.settings",
+ "provider_choice",
+ "value",
+ props.selectResolver,
+ ]);
+ }
+ if (props.hasOwnProperty("inputUriKeys")) {
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: inputUriKeys, waiting for the pref observer"
+ );
+ uriPrefChangedPromise = waitForPrefObserver(TRR_CUSTOM_URI_PREF);
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: inputUriKeys, pref changed, now enter the new value"
+ );
+ let win = doc.ownerGlobal;
+ uriTextbox.focus();
+ uriTextbox.value = props.inputUriKeys;
+ uriTextbox.dispatchEvent(new win.Event("input", { bubbles: true }));
+ uriTextbox.dispatchEvent(new win.Event("change", { bubbles: true }));
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: inputUriKeys, input and change events dispatched"
+ );
+ }
+
+ info(
+ Date.now() -
+ startTime +
+ ": testWithProperties: waiting for any of uri and mode prefs to change"
+ );
+ await Promise.all([
+ uriPrefChangedPromise,
+ modePrefChangedPromise,
+ disableHeuristicsPrefChangedPromise,
+ ]);
+ info(Date.now() - startTime + ": testWithProperties: prefs changed");
+
+ if (props.hasOwnProperty("expectedFinalUriPref")) {
+ if (props.expectedFinalUriPref) {
+ let uriPref = Services.prefs.getStringPref(TRR_URI_PREF);
+ is(
+ uriPref,
+ props.expectedFinalUriPref,
+ "uri pref ended up with the expected value"
+ );
+ } else {
+ ok(
+ !Services.prefs.prefHasUserValue(TRR_URI_PREF),
+ `uri pref ended up with the expected value (unset) got ${Services.prefs.getStringPref(
+ TRR_URI_PREF
+ )}`
+ );
+ }
+ }
+
+ if (props.hasOwnProperty("expectedModePref")) {
+ let modePref = Services.prefs.getIntPref(TRR_MODE_PREF);
+ is(
+ modePref,
+ props.expectedModePref,
+ "mode pref ended up with the expected value"
+ );
+ }
+
+ if (props.hasOwnProperty("expectedDisabledHeuristics")) {
+ let disabledHeuristicsPref = Services.prefs.getBoolPref(
+ HEURISTICS_DISABLED_PREF
+ );
+ is(
+ disabledHeuristicsPref,
+ props.expectedDisabledHeuristics,
+ "disable-heuristics pref ended up with the expected value"
+ );
+ }
+
+ if (props.hasOwnProperty("expectedFinalCustomUriPref")) {
+ let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF);
+ is(
+ customUriPref,
+ props.expectedFinalCustomUriPref,
+ "custom_uri pref ended up with the expected value"
+ );
+ }
+
+ if (props.hasOwnProperty("expectedModeValue")) {
+ let modeValue = Services.prefs.getIntPref(TRR_MODE_PREF);
+ is(modeValue, props.expectedModeValue, "mode pref has expected value");
+ }
+
+ gBrowser.removeCurrentTab();
+ info(Date.now() - startTime + ": testWithProperties: fin");
+}
+
+add_task(async function default_values() {
+ let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF);
+ let uriPrefHasUserValue = Services.prefs.prefHasUserValue(TRR_URI_PREF);
+ let modePref = Services.prefs.getIntPref(TRR_MODE_PREF);
+ is(
+ modePref,
+ defaultPrefValues[TRR_MODE_PREF],
+ `Actual value of ${TRR_MODE_PREF} matches expected default value`
+ );
+ ok(
+ !uriPrefHasUserValue,
+ `Actual value of ${TRR_URI_PREF} matches expected default value (unset)`
+ );
+ is(
+ customUriPref,
+ defaultPrefValues[TRR_CUSTOM_URI_PREF],
+ `Actual value of ${TRR_CUSTOM_URI_PREF} matches expected default value`
+ );
+});
+
+const DEFAULT_OPTION_INDEX = 0;
+const ENABLED_OPTION_INDEX = 1;
+const STRICT_OPTION_INDEX = 2;
+const OFF_OPTION_INDEX = 3;
+
+let testVariations = [
+ // verify state with defaults
+ {
+ name: "default",
+ expectedModePref: 0,
+ expectedSelectedIndex: DEFAULT_OPTION_INDEX,
+ expectedUriValue: "",
+ },
+
+ // verify each of the modes maps to the correct checked state
+ {
+ name: "mode 0",
+ [TRR_MODE_PREF]: 0,
+ expectedSelectedIndex: DEFAULT_OPTION_INDEX,
+ },
+ {
+ name: "mode 1",
+ [TRR_MODE_PREF]: 1,
+ expectedSelectedIndex: OFF_OPTION_INDEX,
+ },
+ {
+ name: "mode 2",
+ [TRR_MODE_PREF]: 2,
+ expectedSelectedIndex: ENABLED_OPTION_INDEX,
+ expectedFinalUriPref: "",
+ },
+ {
+ name: "mode 3",
+ [TRR_MODE_PREF]: 3,
+ expectedSelectedIndex: STRICT_OPTION_INDEX,
+ expectedFinalUriPref: "",
+ },
+ {
+ name: "mode 4",
+ [TRR_MODE_PREF]: 4,
+ expectedSelectedIndex: OFF_OPTION_INDEX,
+ },
+ {
+ name: "mode 5",
+ [TRR_MODE_PREF]: 5,
+ expectedSelectedIndex: OFF_OPTION_INDEX,
+ },
+ // verify an out of bounds mode value maps to the correct checked state
+ {
+ name: "mode out-of-bounds",
+ [TRR_MODE_PREF]: 77,
+ expectedSelectedIndex: OFF_OPTION_INDEX,
+ },
+
+ // verify automatic heuristics states
+ {
+ name: "heuristics on and mode unset",
+ [TRR_MODE_PREF]: 0,
+ [ROLLOUT_ENABLED_PREF]: true,
+ expectedSelectedIndex: DEFAULT_OPTION_INDEX,
+ },
+ {
+ name: "heuristics on and mode set to 2",
+ [TRR_MODE_PREF]: 2,
+ [ROLLOUT_ENABLED_PREF]: true,
+ expectedSelectedIndex: ENABLED_OPTION_INDEX,
+ },
+ {
+ name: "heuristics on but disabled, mode unset",
+ [TRR_MODE_PREF]: 5,
+ [ROLLOUT_ENABLED_PREF]: true,
+ expectedSelectedIndex: OFF_OPTION_INDEX,
+ },
+ {
+ name: "heuristics on but disabled, mode set to 2",
+ [TRR_MODE_PREF]: 2,
+ [ROLLOUT_ENABLED_PREF]: true,
+ expectedSelectedIndex: ENABLED_OPTION_INDEX,
+ },
+
+ // verify picking each radio button option gives the right outcomes
+ {
+ name: "toggle mode on",
+ clickMode: "dohEnabledRadio",
+ expectedModeValue: 2,
+ expectedUriValue: "",
+ expectedFinalUriPref: "",
+ },
+ {
+ name: "toggle mode off",
+ [TRR_MODE_PREF]: 2,
+ expectedSelectedIndex: ENABLED_OPTION_INDEX,
+ clickMode: "dohOffRadio",
+ expectedModePref: 5,
+ },
+ {
+ name: "toggle mode off when on due to heuristics",
+ [TRR_MODE_PREF]: 0,
+ [ROLLOUT_ENABLED_PREF]: true,
+ expectedSelectedIndex: DEFAULT_OPTION_INDEX,
+ clickMode: "dohOffRadio",
+ expectedModePref: 5,
+ expectedDisabledHeuristics: true,
+ },
+ // Test selecting non-default, non-custom TRR provider, NextDNS.
+ {
+ name: "Select NextDNS as TRR provider",
+ [TRR_MODE_PREF]: 2,
+ selectResolver: SECOND_RESOLVER_VALUE,
+ expectedFinalUriPref: SECOND_RESOLVER_VALUE,
+ },
+ // Test selecting non-default, non-custom TRR provider, NextDNS,
+ // with DoH not enabled. The provider selection should stick.
+ {
+ name: "Select NextDNS as TRR provider in mode 0",
+ [TRR_MODE_PREF]: 0,
+ selectResolver: SECOND_RESOLVER_VALUE,
+ expectedFinalUriPref: SECOND_RESOLVER_VALUE,
+ },
+ {
+ name: "return to default from NextDNS",
+ [TRR_MODE_PREF]: 2,
+ [TRR_URI_PREF]: SECOND_RESOLVER_VALUE,
+ expectedResolverListValue: SECOND_RESOLVER_VALUE,
+ selectResolver: DEFAULT_RESOLVER_VALUE,
+ expectedFinalUriPref: FIRST_RESOLVER_VALUE,
+ },
+ // test that selecting Custom, when we have a TRR_CUSTOM_URI_PREF subsequently changes TRR_URI_PREF
+ {
+ name: "select custom with existing custom_uri pref value",
+ [TRR_MODE_PREF]: 2,
+ [TRR_CUSTOM_URI_PREF]: "https://example.com",
+ expectedModeValue: 2,
+ expectedSelectedIndex: ENABLED_OPTION_INDEX,
+ selectResolver: "custom",
+ expectedUriValue: "https://example.com",
+ expectedFinalUriPref: "https://example.com",
+ expectedFinalCustomUriPref: "https://example.com",
+ },
+ {
+ name: "select custom and enter new custom_uri pref value",
+ [TRR_URI_PREF]: "",
+ [TRR_CUSTOM_URI_PREF]: "",
+ clickMode: "dohEnabledRadio",
+ selectResolver: "custom",
+ inputUriKeys: "https://custom.com",
+ expectedModePref: 2,
+ expectedFinalUriPref: "https://custom.com",
+ expectedFinalCustomUriPref: "https://custom.com",
+ },
+
+ {
+ name: "return to default from custom",
+ [TRR_MODE_PREF]: 2,
+ [TRR_URI_PREF]: "https://example.com",
+ [TRR_CUSTOM_URI_PREF]: "https://custom.com",
+ expectedUriValue: "https://example.com",
+ expectedResolverListValue: "custom",
+ selectResolver: DEFAULT_RESOLVER_VALUE,
+ expectedFinalUriPref: DEFAULT_RESOLVER_VALUE,
+ expectedFinalCustomUriPref: "https://example.com",
+ },
+ {
+ name: "clear the custom uri",
+ [TRR_MODE_PREF]: 2,
+ [TRR_URI_PREF]: "https://example.com",
+ [TRR_CUSTOM_URI_PREF]: "https://example.com",
+ expectedUriValue: "https://example.com",
+ expectedResolverListValue: "custom",
+ inputUriKeys: "",
+ expectedFinalUriPref: " ",
+ expectedFinalCustomUriPref: "",
+ },
+ {
+ name: "empty default resolver list",
+ [TRR_MODE_PREF]: 2,
+ [TRR_URI_PREF]: "https://example.com",
+ [TRR_CUSTOM_URI_PREF]: "",
+ expectedUriValue: "https://example.com",
+ expectedResolverListValue: "custom",
+ expectedFinalUriPref: "https://example.com",
+ expectedFinalCustomUriPref: "https://example.com",
+ },
+];
+
+for (let props of testVariations) {
+ add_task(async function testVariation() {
+ let startTime = Date.now();
+ info("starting test: " + props.name);
+ await testWithProperties(props, startTime);
+ await resetPrefs();
+ });
+}
+
+add_task(async function testRemoteSettingsEnable() {
+ let startTime = Date.now();
+ // Enable the rollout.
+ await DoHTestUtils.loadRemoteSettingsConfig({
+ providers: "example-1, example-2",
+ rolloutEnabled: true,
+ steeringEnabled: false,
+ steeringProviders: "",
+ autoDefaultEnabled: false,
+ autoDefaultProviders: "",
+ id: "global",
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ info(Date.now() - startTime + ": testWithProperties: tab now open");
+ let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup");
+
+ is(modeRadioGroup.value, "0", "expecting default mode");
+
+ let status = doc.getElementById("dohStatus");
+ await TestUtils.waitForCondition(
+ () => document.l10n.getAttributes(status).args.status == "Active"
+ );
+ is(
+ document.l10n.getAttributes(status).args.status,
+ "Active",
+ "expecting status active"
+ );
+
+ let provider = doc.getElementById("dohResolver");
+ is(
+ provider.hidden,
+ false,
+ "Provider should not be hidden when status is active"
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ document.l10n.getAttributes(provider).args.name ==
+ DoHConfigController.currentConfig.providerList[0].UIName
+ );
+ is(
+ document.l10n.getAttributes(provider).args.name,
+ DoHConfigController.currentConfig.providerList[0].UIName,
+ "expecting the right provider name"
+ );
+
+ let option = doc.getElementById("dohEnabledRadio");
+ option.scrollIntoView();
+ let win = doc.ownerGlobal;
+ EventUtils.synthesizeMouseAtCenter(option, {}, win);
+
+ await TestUtils.waitForCondition(() =>
+ Services.prefs.prefHasUserValue("doh-rollout.disable-heuristics")
+ );
+ is(provider.hidden, false);
+ await TestUtils.waitForCondition(
+ () =>
+ document.l10n.getAttributes(provider).args.name ==
+ DoHConfigController.currentConfig.providerList[0].UIName
+ );
+ is(
+ document.l10n.getAttributes(provider).args.name,
+ DoHConfigController.currentConfig.providerList[0].UIName,
+ "expecting the right provider name"
+ );
+ is(
+ Services.prefs.getIntPref("network.trr.mode"),
+ Ci.nsIDNSService.MODE_TRRFIRST
+ );
+
+ option = doc.getElementById("dohOffRadio");
+ option.scrollIntoView();
+ win = doc.ownerGlobal;
+ EventUtils.synthesizeMouseAtCenter(option, {}, win);
+ await TestUtils.waitForCondition(() => status.innerHTML == "Status: Off");
+ is(
+ Services.prefs.getIntPref("network.trr.mode"),
+ Ci.nsIDNSService.MODE_TRROFF
+ );
+ is(provider.hidden, true, "Expecting provider to be hidden when DoH is off");
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testEnterprisePolicy() {
+ async function withPolicy(policy, fn, preFn = () => {}) {
+ await resetPrefs();
+ PoliciesPrefTracker.start();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy);
+ await preFn();
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup");
+ let resolverMenulist = doc.getElementById("dohEnabledResolverChoices");
+ let uriTextbox = doc.getElementById("dohEnabledInputField");
+
+ await fn({
+ modeRadioGroup,
+ resolverMenulist,
+ doc,
+ uriTextbox,
+ });
+
+ gBrowser.removeCurrentTab();
+ EnterprisePolicyTesting.resetRunOnceState();
+ PoliciesPrefTracker.stop();
+ }
+
+ info("Check that a locked policy does not allow any changes in the UI");
+ await withPolicy(
+ {
+ policies: {
+ DNSOverHTTPS: {
+ Enabled: true,
+ ProviderURL: "https://examplelocked.com/provider",
+ ExcludedDomains: ["examplelocked.com", "example.org"],
+ Locked: true,
+ },
+ },
+ },
+ async res => {
+ is(res.modeRadioGroup.disabled, true, "The mode menu should be locked.");
+ is(res.modeRadioGroup.value, "2", "Should be enabled");
+ is(res.resolverMenulist.value, "custom", "Resolver list shows custom");
+ is(
+ res.uriTextbox.value,
+ "https://examplelocked.com/provider",
+ "Custom URI should be set"
+ );
+ }
+ );
+
+ info("Check that an unlocked policy has editable fields in the dialog");
+ await withPolicy(
+ {
+ policies: {
+ DNSOverHTTPS: {
+ Enabled: true,
+ ProviderURL: "https://example.com/provider",
+ ExcludedDomains: ["example.com", "example.org"],
+ },
+ },
+ },
+ async res => {
+ is(
+ res.modeRadioGroup.disabled,
+ false,
+ "The mode menu should not be locked."
+ );
+ is(res.modeRadioGroup.value, "2", "Should be enabled");
+ is(res.resolverMenulist.value, "custom", "Resolver list shows custom");
+ is(
+ res.uriTextbox.value,
+ "https://example.com/provider",
+ "Expected custom resolver"
+ );
+ }
+ );
+
+ info("Check that a locked disabled policy disables the buttons");
+ await withPolicy(
+ {
+ policies: {
+ DNSOverHTTPS: {
+ Enabled: false,
+ ProviderURL: "https://example.com/provider",
+ ExcludedDomains: ["example.com", "example.org"],
+ Locked: true,
+ },
+ },
+ },
+ async res => {
+ is(res.modeRadioGroup.disabled, true, "The mode menu should be locked.");
+ is(res.modeRadioGroup.value, "5", "Should be disabled");
+ }
+ );
+
+ info("Check that an unlocked disabled policy has editable fields");
+ await withPolicy(
+ {
+ policies: {
+ DNSOverHTTPS: {
+ Enabled: false,
+ ProviderURL: "https://example.com/provider",
+ ExcludedDomains: ["example.com", "example.org"],
+ },
+ },
+ },
+ async res => {
+ is(
+ res.modeRadioGroup.disabled,
+ false,
+ "The mode menu should not be locked."
+ );
+ is(res.modeRadioGroup.value, "5", "Should be disabled");
+ }
+ );
+
+ info("Check that the remote settings config doesn't override the policy");
+ await withPolicy(
+ {
+ policies: {
+ DNSOverHTTPS: {
+ Enabled: true,
+ ProviderURL: "https://example.com/provider",
+ ExcludedDomains: ["example.com", "example.org"],
+ },
+ },
+ },
+ async res => {
+ is(
+ res.modeRadioGroup.disabled,
+ false,
+ "The mode menu should not be locked."
+ );
+ is(res.resolverMenulist.value, "custom", "Resolver list shows custom");
+ is(
+ res.uriTextbox.value,
+ "https://example.com/provider",
+ "Expected custom resolver"
+ );
+ },
+ async function runAfterSettingPolicy() {
+ await DoHTestUtils.loadRemoteSettingsConfig({
+ providers: "example-1, example-2",
+ rolloutEnabled: true,
+ steeringEnabled: false,
+ steeringProviders: "",
+ autoDefaultEnabled: false,
+ autoDefaultProviders: "",
+ id: "global",
+ });
+ }
+ );
+});
+
+add_task(async function clickWarnButton() {
+ Services.prefs.setBoolPref(
+ "network.trr_ui.show_fallback_warning_option",
+ true
+ );
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ await clearEvents();
+ let checkbox = doc.getElementById("dohWarnCheckbox1");
+ checkbox.click();
+
+ let event = await getEvent("security.doh.settings", "warn_checkbox");
+ Assert.deepEqual(event, [
+ "security.doh.settings",
+ "warn_checkbox",
+ "checkbox",
+ "true",
+ ]);
+ Assert.equal(
+ Services.prefs.getBoolPref("network.trr.display_fallback_warning"),
+ true,
+ "Clicking the checkbox should change the pref"
+ );
+
+ checkbox.click();
+ event = await getEvent("security.doh.settings", "warn_checkbox");
+ Assert.deepEqual(event, [
+ "security.doh.settings",
+ "warn_checkbox",
+ "checkbox",
+ "false",
+ ]);
+ Assert.equal(
+ Services.prefs.getBoolPref("network.trr.display_fallback_warning"),
+ false,
+ "Clicking the checkbox should change the pref"
+ );
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js b/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js
new file mode 100644
index 0000000000..883e19acc2
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js
@@ -0,0 +1,855 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests the Privacy pane's Firefox Suggest UI.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+const CONTAINER_ID = "firefoxSuggestContainer";
+const NONSPONSORED_TOGGLE_ID = "firefoxSuggestNonsponsoredToggle";
+const SPONSORED_TOGGLE_ID = "firefoxSuggestSponsoredToggle";
+const DATA_COLLECTION_TOGGLE_ID = "firefoxSuggestDataCollectionToggle";
+const INFO_BOX_ID = "firefoxSuggestInfoBox";
+const INFO_TEXT_ID = "firefoxSuggestInfoText";
+const LEARN_MORE_CLASS = "firefoxSuggestLearnMore";
+const BEST_MATCH_CONTAINER_ID = "firefoxSuggestBestMatchContainer";
+const BEST_MATCH_CHECKBOX_ID = "firefoxSuggestBestMatch";
+const BUTTON_RESTORE_DISMISSED_ID = "restoreDismissedSuggestions";
+const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST =
+ "browser.urlbar.quicksuggest.blockedDigests";
+const PREF_URLBAR_WEATHER_USER_ENABLED = "browser.urlbar.suggest.weather";
+
+// Maps text element IDs to `{ enabled, disabled }`, where `enabled` is the
+// expected l10n ID when the Firefox Suggest feature is enabled, and `disabled`
+// is when disabled.
+const EXPECTED_L10N_IDS = {
+ locationBarGroupHeader: {
+ enabled: "addressbar-header-firefox-suggest",
+ disabled: "addressbar-header",
+ },
+ locationBarSuggestionLabel: {
+ enabled: "addressbar-suggest-firefox-suggest",
+ disabled: "addressbar-suggest",
+ },
+};
+
+// This test can take a while due to the many permutations some of these tasks
+// run through, so request a longer timeout.
+requestLongerTimeout(10);
+
+// The following tasks check the visibility of the Firefox Suggest UI based on
+// the value of the feature pref. See doVisibilityTest().
+
+add_task(async function historyToOffline() {
+ await doVisibilityTest({
+ initialScenario: "history",
+ initialExpectedVisibility: false,
+ newScenario: "offline",
+ newExpectedVisibility: true,
+ });
+});
+
+add_task(async function historyToOnline() {
+ await doVisibilityTest({
+ initialScenario: "history",
+ initialExpectedVisibility: false,
+ newScenario: "online",
+ newExpectedVisibility: true,
+ });
+});
+
+add_task(async function offlineToHistory() {
+ await doVisibilityTest({
+ initialScenario: "offline",
+ initialExpectedVisibility: true,
+ newScenario: "history",
+ newExpectedVisibility: false,
+ });
+});
+
+add_task(async function offlineToOnline() {
+ await doVisibilityTest({
+ initialScenario: "offline",
+ initialExpectedVisibility: true,
+ newScenario: "online",
+ newExpectedVisibility: true,
+ });
+});
+
+add_task(async function onlineToHistory() {
+ await doVisibilityTest({
+ initialScenario: "online",
+ initialExpectedVisibility: true,
+ newScenario: "history",
+ newExpectedVisibility: false,
+ });
+});
+
+add_task(async function onlineToOffline() {
+ await doVisibilityTest({
+ initialScenario: "online",
+ initialExpectedVisibility: true,
+ newScenario: "offline",
+ newExpectedVisibility: true,
+ });
+});
+
+/**
+ * Runs a test that checks the visibility of the Firefox Suggest preferences UI
+ * based on scenario pref.
+ *
+ * @param {string} initialScenario
+ * The initial scenario.
+ * @param {boolean} initialExpectedVisibility
+ * Whether the UI should be visible with the initial scenario.
+ * @param {string} newScenario
+ * The updated scenario.
+ * @param {boolean} newExpectedVisibility
+ * Whether the UI should be visible after setting the new scenario.
+ */
+async function doVisibilityTest({
+ initialScenario,
+ initialExpectedVisibility,
+ newScenario,
+ newExpectedVisibility,
+}) {
+ info(
+ "Running visibility test: " +
+ JSON.stringify(
+ {
+ initialScenario,
+ initialExpectedVisibility,
+ newScenario,
+ newExpectedVisibility,
+ },
+ null,
+ 2
+ )
+ );
+
+ // Set the initial scenario.
+ await QuickSuggestTestUtils.setScenario(initialScenario);
+
+ Assert.equal(
+ Services.prefs.getBoolPref("browser.urlbar.quicksuggest.enabled"),
+ initialExpectedVisibility,
+ `quicksuggest.enabled is correct after setting initial scenario, initialExpectedVisibility=${initialExpectedVisibility}`
+ );
+
+ // Open prefs and check the initial visibility.
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let container = doc.getElementById(CONTAINER_ID);
+ Assert.equal(
+ BrowserTestUtils.is_visible(container),
+ initialExpectedVisibility,
+ `The container has the expected initial visibility, initialExpectedVisibility=${initialExpectedVisibility}`
+ );
+
+ // Check the text elements' l10n IDs.
+ for (let [id, { enabled, disabled }] of Object.entries(EXPECTED_L10N_IDS)) {
+ Assert.equal(
+ doc.getElementById(id).dataset.l10nId,
+ initialExpectedVisibility ? enabled : disabled,
+ `Initial l10n ID for element with ID ${id}, initialExpectedVisibility=${initialExpectedVisibility}`
+ );
+ }
+
+ // Set the new scenario.
+ await QuickSuggestTestUtils.setScenario(newScenario);
+
+ Assert.equal(
+ Services.prefs.getBoolPref("browser.urlbar.quicksuggest.enabled"),
+ newExpectedVisibility,
+ `quicksuggest.enabled is correct after setting new scenario, newExpectedVisibility=${newExpectedVisibility}`
+ );
+
+ // Check visibility again.
+ Assert.equal(
+ BrowserTestUtils.is_visible(container),
+ newExpectedVisibility,
+ `The container has the expected visibility after setting new scenario, newExpectedVisibility=${newExpectedVisibility}`
+ );
+
+ // Check the text elements' l10n IDs again.
+ for (let [id, { enabled, disabled }] of Object.entries(EXPECTED_L10N_IDS)) {
+ Assert.equal(
+ doc.getElementById(id).dataset.l10nId,
+ newExpectedVisibility ? enabled : disabled,
+ `New l10n ID for element with ID ${id}, newExpectedVisibility=${newExpectedVisibility}`
+ );
+ }
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ await QuickSuggestTestUtils.setScenario(null);
+}
+
+// Verifies all 8 states of the 3 toggles and their related info box states.
+add_task(async function togglesAndInfoBox() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ // suggest.quicksuggest.nonsponsored = true
+ // suggest.quicksuggest.sponsored = true
+ // quicksuggest.dataCollection.enabled = true
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", true],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", true],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: true,
+ [SPONSORED_TOGGLE_ID]: true,
+ [DATA_COLLECTION_TOGGLE_ID]: true,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-all");
+ await SpecialPowers.popPrefEnv();
+
+ // suggest.quicksuggest.nonsponsored = true
+ // suggest.quicksuggest.sponsored = true
+ // quicksuggest.dataCollection.enabled = false
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", true],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", true],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", false],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: true,
+ [SPONSORED_TOGGLE_ID]: true,
+ [DATA_COLLECTION_TOGGLE_ID]: false,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored-sponsored");
+ await SpecialPowers.popPrefEnv();
+
+ // suggest.quicksuggest.nonsponsored = true
+ // suggest.quicksuggest.sponsored = false
+ // quicksuggest.dataCollection.enabled = true
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", true],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", false],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: true,
+ [SPONSORED_TOGGLE_ID]: false,
+ [DATA_COLLECTION_TOGGLE_ID]: true,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored-data");
+ await SpecialPowers.popPrefEnv();
+
+ // suggest.quicksuggest.nonsponsored = true
+ // suggest.quicksuggest.sponsored = false
+ // quicksuggest.dataCollection.enabled = false
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", true],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", false],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", false],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: true,
+ [SPONSORED_TOGGLE_ID]: false,
+ [DATA_COLLECTION_TOGGLE_ID]: false,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored");
+ await SpecialPowers.popPrefEnv();
+
+ // suggest.quicksuggest.nonsponsored = false
+ // suggest.quicksuggest.sponsored = true
+ // quicksuggest.dataCollection.enabled = true
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", false],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", true],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: false,
+ [SPONSORED_TOGGLE_ID]: true,
+ [DATA_COLLECTION_TOGGLE_ID]: true,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-sponsored-data");
+ await SpecialPowers.popPrefEnv();
+
+ // suggest.quicksuggest.nonsponsored = false
+ // suggest.quicksuggest.sponsored = true
+ // quicksuggest.dataCollection.enabled = false
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", false],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", true],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", false],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: false,
+ [SPONSORED_TOGGLE_ID]: true,
+ [DATA_COLLECTION_TOGGLE_ID]: false,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-sponsored");
+ await SpecialPowers.popPrefEnv();
+
+ // suggest.quicksuggest.nonsponsored = false
+ // suggest.quicksuggest.sponsored = false
+ // quicksuggest.dataCollection.enabled = true
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", false],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", false],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: false,
+ [SPONSORED_TOGGLE_ID]: false,
+ [DATA_COLLECTION_TOGGLE_ID]: true,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-data");
+ await SpecialPowers.popPrefEnv();
+
+ // suggest.quicksuggest.nonsponsored = false
+ // suggest.quicksuggest.sponsored = false
+ // quicksuggest.dataCollection.enabled = false
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", false],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", false],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", false],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: false,
+ [SPONSORED_TOGGLE_ID]: false,
+ [DATA_COLLECTION_TOGGLE_ID]: false,
+ });
+ await assertInfoBox(null);
+ await SpecialPowers.popPrefEnv();
+
+ gBrowser.removeCurrentTab();
+});
+
+// Clicks each of the toggles and makes sure the prefs and info box are updated.
+add_task(async function clickToggles() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let addressBarSection = doc.getElementById("locationBarGroup");
+ addressBarSection.scrollIntoView();
+
+ async function clickToggle(id) {
+ let toggle = doc.getElementById(id);
+ let changed = BrowserTestUtils.waitForEvent(toggle, "toggle");
+ let button = toggle.buttonEl;
+ await EventUtils.synthesizeMouseAtCenter(
+ button,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+ await changed;
+ }
+
+ // Set initial state.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", true],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", true],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: true,
+ [SPONSORED_TOGGLE_ID]: true,
+ [DATA_COLLECTION_TOGGLE_ID]: true,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-all");
+
+ // non-sponsored toggle
+ await clickToggle(NONSPONSORED_TOGGLE_ID);
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.suggest.quicksuggest.nonsponsored"
+ ),
+ "suggest.quicksuggest.nonsponsored is false after clicking non-sponsored toggle"
+ );
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: false,
+ [SPONSORED_TOGGLE_ID]: true,
+ [DATA_COLLECTION_TOGGLE_ID]: true,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-sponsored-data");
+
+ // sponsored toggle
+ await clickToggle(SPONSORED_TOGGLE_ID);
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.suggest.quicksuggest.nonsponsored"
+ ),
+ "suggest.quicksuggest.nonsponsored remains false after clicking sponsored toggle"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.suggest.quicksuggest.sponsored"
+ ),
+ "suggest.quicksuggest.sponsored is false after clicking sponsored toggle"
+ );
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: false,
+ [SPONSORED_TOGGLE_ID]: false,
+ [DATA_COLLECTION_TOGGLE_ID]: true,
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-data");
+
+ // data collection toggle
+ await clickToggle(DATA_COLLECTION_TOGGLE_ID);
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.suggest.quicksuggest.nonsponsored"
+ ),
+ "suggest.quicksuggest.nonsponsored remains false after clicking sponsored toggle"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.suggest.quicksuggest.sponsored"
+ ),
+ "suggest.quicksuggest.sponsored remains false after clicking data collection toggle"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.quicksuggest.dataCollection.enabled"
+ ),
+ "quicksuggest.dataCollection.enabled is false after clicking data collection toggle"
+ );
+ assertPrefUIState({
+ [NONSPONSORED_TOGGLE_ID]: false,
+ [SPONSORED_TOGGLE_ID]: false,
+ [DATA_COLLECTION_TOGGLE_ID]: false,
+ });
+ await assertInfoBox(null);
+
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Clicks the learn-more links and checks the help page is opened in a new tab.
+add_task(async function clickLearnMore() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let addressBarSection = doc.getElementById("locationBarGroup");
+ addressBarSection.scrollIntoView();
+
+ // Set initial state so that the info box and learn more link are shown.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quicksuggest.nonsponsored", true],
+ ["browser.urlbar.suggest.quicksuggest.sponsored", true],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+ await assertInfoBox("addressbar-firefox-suggest-info-all");
+
+ let learnMoreLinks = doc.querySelectorAll("." + LEARN_MORE_CLASS);
+ Assert.equal(
+ learnMoreLinks.length,
+ 3,
+ "Expected number of learn-more links are present"
+ );
+ for (let link of learnMoreLinks) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(link),
+ "Learn-more link is visible: " + link.id
+ );
+ }
+
+ let prefsTab = gBrowser.selectedTab;
+ for (let link of learnMoreLinks) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ );
+ info("Clicking learn-more link: " + link.id);
+ Assert.ok(link.id, "Sanity check: Learn-more link has an ID");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + link.id,
+ {},
+ gBrowser.selectedBrowser
+ );
+ info("Waiting for help page to load in a new tab");
+ await tabPromise;
+ gBrowser.removeCurrentTab();
+ gBrowser.selectedTab = prefsTab;
+ }
+
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests the visibility of the best match checkbox based on the values of
+// `browser.urlbar.quicksuggest.enabled` and `browser.urlbar.bestMatch.enabled`.
+add_task(async function bestMatchVisibility() {
+ for (let initialQuickSuggest of [false, true]) {
+ for (let initialBestMatch of [false, true]) {
+ for (let newQuickSuggest of [false, true]) {
+ for (let newBestMatch of [false, true]) {
+ await doBestMatchVisibilityTest({
+ initialQuickSuggest,
+ initialBestMatch,
+ newQuickSuggest,
+ newBestMatch,
+ });
+ }
+ }
+ }
+ }
+});
+
+// Tests the "Restore" button for dismissed suggestions.
+add_task(async function restoreDismissedSuggestions() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let addressBarSection = doc.getElementById("locationBarGroup");
+ addressBarSection.scrollIntoView();
+
+ let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID);
+ Assert.equal(
+ Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""),
+ "",
+ "Block list is empty initially"
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED),
+ "Weather suggestions are enabled initially"
+ );
+ Assert.ok(button.disabled, "Restore button is disabled initially.");
+
+ await QuickSuggest.blockedSuggestions.add("https://example.com/");
+ Assert.notEqual(
+ Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""),
+ "",
+ "Block list is non-empty after adding URL"
+ );
+ Assert.ok(!button.disabled, "Restore button is enabled after blocking URL.");
+ button.click();
+ Assert.equal(
+ Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""),
+ "",
+ "Block list is empty clicking Restore button"
+ );
+ Assert.ok(button.disabled, "Restore button is disabled after clicking it.");
+
+ Services.prefs.setBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED, false);
+ Assert.ok(
+ !button.disabled,
+ "Restore button is enabled after disabling weather suggestions."
+ );
+ button.click();
+ Assert.ok(
+ Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED),
+ "Weather suggestions are enabled after clicking Restore button"
+ );
+ Assert.ok(
+ button.disabled,
+ "Restore button is disabled after clicking it again."
+ );
+
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Runs a test that checks the visibility of the Firefox Suggest best match
+ * checkbox. It does the following:
+ *
+ * 1. Sets the quick suggest and best match feature prefs
+ * 2. Opens about:preferences and checks the visibility of the checkbox
+ * 3. Sets the quick suggest and best match feature prefs again
+ * 4. Checks the visibility of the checkbox again
+ *
+ * @param {boolean} initialQuickSuggest
+ * The value to set for `browser.urlbar.quicksuggest.enabled` before
+ * about:preferences is opened.
+ * @param {boolean} initialBestMatch
+ * The value to set for `browser.urlbar.bestMatch.enabled` before
+ * about:preferences is opened.
+ * @param {boolean} newQuickSuggest
+ * The value to set for `browser.urlbar.quicksuggest.enabled` while
+ * about:preferences is open.
+ * @param {boolean} newBestMatch
+ * The value to set for `browser.urlbar.bestMatch.enabled` while
+ * about:preferences is open.
+ */
+async function doBestMatchVisibilityTest({
+ initialQuickSuggest,
+ initialBestMatch,
+ newQuickSuggest,
+ newBestMatch,
+}) {
+ info(
+ "Running best match visibility test: " +
+ JSON.stringify(
+ {
+ initialQuickSuggest,
+ initialBestMatch,
+ newQuickSuggest,
+ newBestMatch,
+ },
+ null,
+ 2
+ )
+ );
+
+ // Set the initial pref values.
+ Services.prefs.setBoolPref(
+ "browser.urlbar.quicksuggest.enabled",
+ initialQuickSuggest
+ );
+ Services.prefs.setBoolPref(
+ "browser.urlbar.bestMatch.enabled",
+ initialBestMatch
+ );
+
+ // Open prefs and check the initial visibility.
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let container = doc.getElementById(BEST_MATCH_CONTAINER_ID);
+ Assert.equal(
+ BrowserTestUtils.is_visible(container),
+ initialBestMatch,
+ "The checkbox container has the expected initial visibility"
+ );
+
+ // Set the new pref values.
+ Services.prefs.setBoolPref(
+ "browser.urlbar.quicksuggest.enabled",
+ newQuickSuggest
+ );
+ Services.prefs.setBoolPref("browser.urlbar.bestMatch.enabled", newBestMatch);
+
+ // Check visibility again.
+ Assert.equal(
+ BrowserTestUtils.is_visible(container),
+ newBestMatch,
+ "The checkbox container has the expected visibility after setting prefs"
+ );
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref("browser.urlbar.quicksuggest.enabled");
+ Services.prefs.clearUserPref("browser.urlbar.bestMatch.enabled");
+}
+
+// Tests the visibility of the best match checkbox when the best match feature
+// is enabled via a Nimbus experiment before about:preferences is opened.
+add_task(async function bestMatchVisibility_experiment_beforeOpen() {
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ bestMatchEnabled: true,
+ },
+ callback: async () => {
+ await openPreferencesViaOpenPreferencesAPI("privacy", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let container = doc.getElementById(BEST_MATCH_CONTAINER_ID);
+ Assert.ok(
+ BrowserTestUtils.is_visible(container),
+ "The checkbox container is visible"
+ );
+ gBrowser.removeCurrentTab();
+ },
+ });
+});
+
+// Tests the visibility of the best match checkbox when the best match feature
+// is enabled via a Nimbus experiment after about:preferences is opened.
+add_task(async function bestMatchVisibility_experiment_afterOpen() {
+ // Open prefs and check the initial visibility.
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let container = doc.getElementById(BEST_MATCH_CONTAINER_ID);
+ Assert.ok(
+ BrowserTestUtils.is_hidden(container),
+ "The checkbox container is hidden initially"
+ );
+
+ // Install an experiment with best match enabled.
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ bestMatchEnabled: true,
+ },
+ callback: () => {
+ Assert.ok(
+ BrowserTestUtils.is_visible(container),
+ "The checkbox container is visible after installing the experiment"
+ );
+ },
+ });
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(container),
+ "The checkbox container is hidden again after the experiment is uninstalled"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// Check the pref and the checkbox for best match.
+add_task(async function bestMatchToggle() {
+ // Enable the feature so that the toggle appears.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const checkbox = doc.getElementById(BEST_MATCH_CHECKBOX_ID);
+ checkbox.scrollIntoView();
+
+ info("Check if the checkbox stauts reflects the pref value");
+ for (const isEnabled of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.bestmatch", isEnabled]],
+ });
+ assertPrefUIState({ [BEST_MATCH_CHECKBOX_ID]: isEnabled }, "checked");
+ await SpecialPowers.popPrefEnv();
+ }
+
+ info("Check if the pref value reflects the checkbox status");
+ for (let i = 0; i < 2; i++) {
+ const initialValue = checkbox.checked;
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + BEST_MATCH_CHECKBOX_ID,
+ {},
+ gBrowser.selectedBrowser
+ );
+ Assert.ok(initialValue !== checkbox.checked);
+ Assert.equal(
+ Services.prefs.getBoolPref("browser.urlbar.suggest.bestmatch"),
+ checkbox.checked
+ );
+ }
+
+ // Clean up.
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bestmatch");
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Clicks the learn-more link for best match and checks the help page is opened
+// in a new tab.
+add_task(async function clickBestMatchLearnMore() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const link = doc.getElementById("firefoxSuggestBestMatchLearnMore");
+ Assert.ok(BrowserTestUtils.is_visible(link), "Learn-more link is visible");
+
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ );
+
+ info("Clicking learn-more link");
+ link.scrollIntoView();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#firefoxSuggestBestMatchLearnMore",
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ info("Waiting for help page to load in a new tab");
+ const tab = await tabPromise;
+ gBrowser.removeTab(tab);
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Verifies the state of pref related toggles and checkboxes.
+ *
+ * @param {object} stateByElementID
+ * Maps toggle/checkbox element IDs to booleans. Each boolean
+ * is the expected state of the corresponding ID.
+ * @param {object} attr
+ * Attribute to check against the expected state. The "pressed"
+ * attribute is verified by default, since this is mostly used
+ * for toggle buttons.
+ */
+function assertPrefUIState(stateByElementID, attr = "pressed") {
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let container = doc.getElementById(CONTAINER_ID);
+ Assert.ok(BrowserTestUtils.is_visible(container), "The container is visible");
+ for (let [id, state] of Object.entries(stateByElementID)) {
+ let element = doc.getElementById(id);
+ Assert.equal(element[attr], state, "Expected state for ID: " + id);
+ }
+}
+
+/**
+ * Verifies the state of the info box.
+ *
+ * @param {string} expectedL10nID
+ * The l10n ID of the string that should be visible in the info box, null if
+ * the info box should be hidden.
+ */
+async function assertInfoBox(expectedL10nID) {
+ info("Checking info box with expected l10n ID: " + expectedL10nID);
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let infoBox = doc.getElementById(INFO_BOX_ID);
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(infoBox) == !!expectedL10nID,
+ "Waiting for expected info box visibility: " + !!expectedL10nID
+ );
+
+ let infoIcon = infoBox.querySelector(".info-icon");
+ Assert.equal(
+ BrowserTestUtils.is_visible(infoIcon),
+ !!expectedL10nID,
+ "The info icon is visible iff a description should be shown"
+ );
+
+ let learnMore = infoBox.querySelector("." + LEARN_MORE_CLASS);
+ Assert.ok(learnMore, "Found the info box learn more link");
+ Assert.equal(
+ BrowserTestUtils.is_visible(learnMore),
+ !!expectedL10nID,
+ "The info box learn more link is visible iff a description should be shown"
+ );
+
+ if (expectedL10nID) {
+ let infoText = doc.getElementById(INFO_TEXT_ID);
+ Assert.equal(
+ infoText.dataset.l10nId,
+ expectedL10nID,
+ "Info text has expected l10n ID"
+ );
+ }
+}
diff --git a/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js
new file mode 100644
index 0000000000..6a6e419229
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function initialState() {
+ // check pref permutations to verify the UI opens in the correct state
+ const prefTests = [
+ {
+ initialPrefs: [
+ ["signon.rememberSignons", true],
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", true],
+ ["signon.autofillForms", true],
+ ],
+ expected: "checked",
+ },
+ {
+ initialPrefs: [
+ ["signon.rememberSignons", true],
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", false],
+ ["signon.autofillForms", false],
+ ],
+ expected: "unchecked",
+ },
+ {
+ initialPrefs: [
+ ["signon.rememberSignons", true],
+ ["signon.generation.available", false],
+ ["signon.generation.enabled", false],
+ ],
+ expected: "hidden",
+ },
+ {
+ initialPrefs: [
+ ["signon.rememberSignons", false],
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", true],
+ ["signon.autofillForms", true],
+ ],
+ expected: "disabled",
+ },
+ ];
+ for (let test of prefTests) {
+ // set initial pref values
+ info("initialState, testing with: " + JSON.stringify(test));
+ await SpecialPowers.pushPrefEnv({ set: test.initialPrefs });
+
+ // open about:privacy in a tab
+ // verify expected conditions
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ let doc = browser.contentDocument;
+ let generatePasswordsCheckbox = doc.getElementById("generatePasswords");
+ let autofillFormsCheckbox = doc.getElementById(
+ "passwordAutofillCheckbox"
+ );
+ doc.getElementById("passwordSettings").scrollIntoView();
+
+ info("initialState, assert on expected state:" + test.expected);
+ switch (test.expected) {
+ case "hidden":
+ is_element_hidden(
+ generatePasswordsCheckbox,
+ "#generatePasswords checkbox is hidden"
+ );
+ break;
+ case "checked":
+ is_element_visible(
+ generatePasswordsCheckbox,
+ "#generatePasswords checkbox is visible"
+ );
+ ok(
+ generatePasswordsCheckbox.checked,
+ "#generatePasswords checkbox is checked"
+ );
+ ok(
+ autofillFormsCheckbox.checked,
+ "#passwordAutofillCheckbox is checked"
+ );
+ break;
+ case "unchecked":
+ ok(
+ !generatePasswordsCheckbox.checked,
+ "#generatePasswords checkbox is un-checked"
+ );
+ ok(
+ !autofillFormsCheckbox.checked,
+ "#passwordAutofillCheckbox is un-checked"
+ );
+ break;
+ case "disabled":
+ ok(
+ generatePasswordsCheckbox.disabled,
+ "#generatePasswords checkbox is disabled"
+ );
+ ok(
+ autofillFormsCheckbox.disabled,
+ "#passwordAutofillCheckbox is disabled"
+ );
+ break;
+ default:
+ ok(false, "Unknown expected state: " + test.expected);
+ }
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function toggleGenerationEnabled() {
+ // clicking the checkbox should toggle the pref
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", false],
+ ["signon.rememberSignons", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ let doc = browser.contentDocument;
+ let checkbox = doc.getElementById("generatePasswords");
+
+ info("waiting for the browser to have focus");
+ await SimpleTest.promiseFocus(browser);
+ let prefChanged = TestUtils.waitForPrefChange(
+ "signon.generation.enabled"
+ );
+
+ // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it
+ // so use keyboard events instead
+ checkbox.focus();
+ is(doc.activeElement, checkbox, "checkbox is focused");
+ EventUtils.synthesizeKey(" ");
+
+ info("waiting for pref to change");
+ await prefChanged;
+ ok(checkbox.checked, "#generatePasswords checkbox is checked");
+ ok(
+ Services.prefs.getBoolPref("signon.generation.enabled"),
+ "enabled pref is now true"
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function toggleRememberSignon() {
+ // toggling rememberSignons checkbox should make generation checkbox disabled
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", true],
+ ["signon.rememberSignons", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ let doc = browser.contentDocument;
+ let checkbox = doc.getElementById("savePasswords");
+ let generationCheckbox = doc.getElementById("generatePasswords");
+
+ ok(
+ !generationCheckbox.disabled,
+ "generation checkbox is not initially disabled"
+ );
+
+ info("waiting for the browser to have focus");
+ await SimpleTest.promiseFocus(browser);
+ let prefChanged = TestUtils.waitForPrefChange("signon.rememberSignons");
+
+ // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it
+ // so use keyboard events instead
+ checkbox.focus();
+ is(doc.activeElement, checkbox, "checkbox is focused");
+ EventUtils.synthesizeKey(" ");
+
+ info("waiting for pref to change");
+ await prefChanged;
+ ok(!checkbox.checked, "#savePasswords checkbox is un-checked");
+ ok(generationCheckbox.disabled, "generation checkbox becomes disabled");
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/preferences/tests/browser_privacy_quickactions.js b/browser/components/preferences/tests/browser_privacy_quickactions.js
new file mode 100644
index 0000000000..14f907cc5c
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_quickactions.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests the Privacy pane's Firefox QuickActions UI.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.quickactions.enabled", true],
+ ],
+ });
+
+ UrlbarProviderQuickActions.addAction("testaction", {
+ commands: ["testaction"],
+ label: "quickactions-downloads2",
+ });
+
+ registerCleanupFunction(() => {
+ UrlbarProviderQuickActions.removeAction("testaction");
+ });
+});
+
+async function isGroupHidden(tab) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => content.document.getElementById("quickActionsBox").hidden
+ );
+}
+
+add_task(async function test_show_prefs() {
+ Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", false);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+
+ Assert.ok(
+ await isGroupHidden(tab),
+ "The preferences are hidden when pref disabled"
+ );
+
+ Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", true);
+
+ Assert.ok(
+ !(await isGroupHidden(tab)),
+ "The preferences are shown when pref enabled"
+ );
+
+ Services.prefs.clearUserPref("browser.urlbar.quickactions.showPrefs");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+async function testActionIsShown(window) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "testact",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+ try {
+ let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ return result.providerName == "quickactions";
+ } catch (e) {
+ return false;
+ }
+}
+
+add_task(async function test_prefs() {
+ Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+
+ Assert.ok(
+ !(await testActionIsShown(window)),
+ "Actions are not shown while pref disabled"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let checkbox = content.document.getElementById("enableQuickActions");
+ is(
+ checkbox.checked,
+ false,
+ "Checkbox is not checked while feature is disabled"
+ );
+ checkbox.click();
+ });
+
+ Assert.ok(
+ await testActionIsShown(window),
+ "Actions are shown after user clicks checkbox"
+ );
+
+ Services.prefs.clearUserPref("browser.urlbar.quickactions.showPrefs");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/preferences/tests/browser_privacy_relayIntegration.js b/browser/components/preferences/tests/browser_privacy_relayIntegration.js
new file mode 100644
index 0000000000..23d62f38bd
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_relayIntegration.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function initialState() {
+ // check pref permutations to verify the UI opens in the correct state
+ const prefTests = [
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", undefined],
+ ["signon.rememberSignons", true],
+ ],
+ expected: "hidden",
+ },
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", "available"],
+ ["signon.rememberSignons", true],
+ ],
+ expected: "checked",
+ },
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", "enabled"],
+ ["signon.rememberSignons", true],
+ ],
+ expected: "checked",
+ },
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", "disabled"],
+ ["signon.rememberSignons", true],
+ ],
+ expected: "unchecked",
+ },
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", undefined],
+ ["signon.rememberSignons", false],
+ ],
+ expected: "hidden",
+ },
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", "available"],
+ ["signon.rememberSignons", false],
+ ],
+ expected: "checked",
+ },
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", "enabled"],
+ ["signon.rememberSignons", false],
+ ],
+ expected: "checked",
+ },
+ {
+ initialPrefs: [
+ ["signon.firefoxRelay.feature", "disabled"],
+ ["signon.rememberSignons", false],
+ ],
+ expected: "unchecked",
+ },
+ ];
+ for (let test of prefTests) {
+ // set initial pref values
+ info("initialState, testing with: " + JSON.stringify(test));
+ await SpecialPowers.pushPrefEnv({ set: test.initialPrefs });
+
+ // open about:privacy in a tab
+ // verify expected conditions
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ const doc = browser.contentDocument;
+ const relayGroup = doc.getElementById("relayIntegrationBox");
+ const checkbox = doc.getElementById("relayIntegration");
+ const savePasswords = doc.getElementById("savePasswords");
+ doc.getElementById("passwordSettings").scrollIntoView();
+
+ Assert.equal(
+ checkbox.disabled,
+ !savePasswords.checked,
+ "#relayIntegration checkbox disabled when #passwordAutofillCheckbox is unchecked"
+ );
+
+ switch (test.expected) {
+ case "hidden":
+ is_element_hidden(relayGroup, "#relayIntegrationBox is hidden");
+ break;
+ case "checked":
+ is_element_visible(relayGroup, "#relayIntegrationBox is visible");
+ Assert.ok(
+ checkbox.checked,
+ "#relayIntegration checkbox is checked"
+ );
+ break;
+ case "unchecked":
+ is_element_visible(relayGroup, "#relayIntegrationBox is visible");
+ Assert.ok(
+ !checkbox.checked,
+ "#relayIntegration checkbox is un-checked"
+ );
+ break;
+ default:
+ Assert.ok(false, "Unknown expected state: " + test.expected);
+ break;
+ }
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function toggleRelayIntegration() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.firefoxRelay.feature", "enabled"],
+ ["signon.rememberSignons", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async browser => {
+ await SimpleTest.promiseFocus(browser);
+
+ // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it
+ // so use keyboard events instead
+ const doc = browser.contentDocument;
+ const relayCheckbox = doc.getElementById("relayIntegration");
+ relayCheckbox.focus();
+ Assert.equal(doc.activeElement, relayCheckbox, "checkbox is focused");
+ Assert.equal(
+ relayCheckbox.checked,
+ true,
+ "#relayIntegration checkbox is not checked"
+ );
+
+ async function clickOnFeatureCheckbox(
+ expectedPrefValue,
+ expectedCheckValue,
+ message
+ ) {
+ const prefChanged = TestUtils.waitForPrefChange(
+ "signon.firefoxRelay.feature"
+ );
+ EventUtils.synthesizeKey(" ");
+ await prefChanged;
+ Assert.equal(
+ Services.prefs.getStringPref("signon.firefoxRelay.feature"),
+ expectedPrefValue,
+ message
+ );
+ Assert.equal(
+ relayCheckbox.checked,
+ expectedCheckValue,
+ `#relayIntegration checkbox is ${
+ expectedCheckValue ? "checked" : "unchecked"
+ }`
+ );
+ }
+
+ await clickOnFeatureCheckbox(
+ "disabled",
+ false,
+ 'Turn integration off from "enabled" feature state'
+ );
+ await clickOnFeatureCheckbox(
+ "available",
+ true,
+ 'Turn integration on from "enabled" feature state'
+ );
+ await clickOnFeatureCheckbox(
+ "disabled",
+ false,
+ 'Turn integration off from "enabled" feature state'
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function toggleRememberSignon() {
+ // toggling rememberSignons checkbox should make generation checkbox disabled
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.firefoxRelay.feature", "available"],
+ ["signon.rememberSignons", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ const doc = browser.contentDocument;
+ const checkbox = doc.getElementById("savePasswords");
+ const relayCheckbox = doc.getElementById("relayIntegration");
+
+ Assert.ok(
+ !relayCheckbox.disabled,
+ "generation checkbox is not initially disabled"
+ );
+
+ await SimpleTest.promiseFocus(browser);
+ const prefChanged = TestUtils.waitForPrefChange("signon.rememberSignons");
+
+ // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it
+ // so use keyboard events instead
+ checkbox.focus();
+ Assert.equal(doc.activeElement, checkbox, "checkbox is focused");
+ EventUtils.synthesizeKey(" ");
+
+ await prefChanged;
+ Assert.ok(!checkbox.checked, "#savePasswords checkbox is un-checked");
+ Assert.ok(
+ relayCheckbox.disabled,
+ "Relay integration checkbox becomes disabled"
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testLockedRelayPreference() {
+ // Locking relay preference should disable checkbox
+ Services.prefs.lockPref("signon.firefoxRelay.feature");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ const doc = browser.contentDocument;
+ const relayCheckbox = doc.getElementById("relayIntegration");
+
+ Assert.ok(relayCheckbox.disabled, "Relay checkbox should be disabled");
+ }
+ );
+
+ Services.prefs.unlockPref("signon.firefoxRelay.feature");
+});
diff --git a/browser/components/preferences/tests/browser_privacy_segmentation_pref.js b/browser/components/preferences/tests/browser_privacy_segmentation_pref.js
new file mode 100644
index 0000000000..9b71d91e11
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_segmentation_pref.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the privacy segmentation pref and preferences UI.
+
+"use strict";
+
+const PREF = "browser.dataFeatureRecommendations.enabled";
+const PREF_VISIBILITY = "browser.privacySegmentation.preferences.show";
+
+add_task(async function test_preferences_section() {
+ if (!AppConstants.MOZ_DATA_REPORTING) {
+ ok(true, "Skipping test because data reporting is disabled");
+ return;
+ }
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let section = doc.getElementById("privacySegmentationSection");
+ let sectionHeader = section.querySelector("h2");
+ let sectionDescription = section.querySelector("label");
+ let radioGroup = section.querySelector(
+ "#privacyDataFeatureRecommendationRadioGroup"
+ );
+ let radioEnabled = radioGroup.querySelector(
+ "#privacyDataFeatureRecommendationEnabled"
+ );
+ let radioDisabled = radioGroup.querySelector(
+ "#privacyDataFeatureRecommendationDisabled"
+ );
+
+ for (let show of [false, true]) {
+ Services.prefs.setBoolPref(PREF_VISIBILITY, show);
+ let showStr = show ? "visible" : "hidden";
+
+ is(
+ BrowserTestUtils.is_visible(section),
+ show,
+ `Privacy Segmentation section should be ${showStr}.`
+ );
+ is(
+ BrowserTestUtils.is_visible(sectionHeader),
+ show,
+ `Privacy Segmentation section header should be ${showStr}.`
+ );
+ is(
+ BrowserTestUtils.is_visible(sectionDescription),
+ show,
+ `Privacy Segmentation section description should be ${showStr}.`
+ );
+ is(
+ BrowserTestUtils.is_visible(radioGroup),
+ show,
+ `Privacy Segmentation radio group should be ${showStr}.`
+ );
+
+ // The section is visible, test radio buttons.
+ if (show) {
+ Services.prefs.setBoolPref(PREF, false);
+
+ is(
+ radioGroup.value,
+ "false",
+ "Radio group should reflect initial pref state of false."
+ );
+
+ info("Selecting radio on.");
+ radioEnabled.click();
+ is(
+ Services.prefs.getBoolPref(PREF),
+ true,
+ "Privacy Segmentation should be enabled."
+ );
+
+ info("Selecting radio off.");
+ radioDisabled.click();
+ is(
+ Services.prefs.getBoolPref(PREF),
+ false,
+ "Privacy Segmentation should be disabled."
+ );
+
+ info("Updating pref externally");
+ is(
+ radioGroup.value,
+ "false",
+ "Radio group should reflect initial pref state of false."
+ );
+ Services.prefs.setBoolPref(PREF, true);
+ await BrowserTestUtils.waitForMutationCondition(
+ radioGroup,
+ { attributeFilter: ["value"] },
+ () => radioGroup.value == "true"
+ );
+ is(
+ radioGroup.value,
+ "true",
+ "Updating Privacy Segmentation pref also updates radio group."
+ );
+ }
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.prefs.clearUserPref(PREF_VISIBILITY);
+ Services.prefs.clearUserPref(PREF);
+});
+
+add_task(async function test_preferences_section_data_reporting_disabled() {
+ if (AppConstants.MOZ_DATA_REPORTING) {
+ ok(true, "Skipping test because data reporting is enabled");
+ return;
+ }
+
+ for (let show of [false, true]) {
+ Services.prefs.setBoolPref(PREF_VISIBILITY, show);
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let section = doc.getElementById("privacySegmentationSection");
+ is(
+ !!section,
+ show,
+ "Section should only exist when privacy segmentation section is enabled."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ Services.prefs.clearUserPref(PREF_VISIBILITY);
+});
diff --git a/browser/components/preferences/tests/browser_privacy_syncDataClearing.js b/browser/components/preferences/tests/browser_privacy_syncDataClearing.js
new file mode 100644
index 0000000000..d5b6b904ab
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacy_syncDataClearing.js
@@ -0,0 +1,287 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * With no custom cleaning categories set and sanitizeOnShutdown disabled,
+ * the checkboxes "alwaysClear" and "deleteOnClose" should share the same state.
+ * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box.
+ */
+add_task(async function test_syncWithoutCustomPrefs() {
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ let document = gBrowser.contentDocument;
+ let deleteOnCloseBox = document.getElementById("deleteOnClose");
+ let alwaysClearBox = document.getElementById("alwaysClear");
+
+ ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected");
+ ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected");
+
+ deleteOnCloseBox.click();
+
+ ok(deleteOnCloseBox.checked, "DeleteOnClose is selected");
+ is(
+ deleteOnCloseBox.checked,
+ alwaysClearBox.checked,
+ "DeleteOnClose sets alwaysClear in the same state, selected"
+ );
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ "Cookie cleaning pref is set"
+ );
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"),
+ "Cache cleaning pref is set"
+ );
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ "OfflineApps cleaning pref is set"
+ );
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"),
+ "Downloads cleaning pref is not set"
+ );
+
+ deleteOnCloseBox.click();
+
+ ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected");
+ is(
+ deleteOnCloseBox.checked,
+ alwaysClearBox.checked,
+ "DeleteOnclose sets alwaysClear in the same state, deselected"
+ );
+
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ "Cookie cleaning pref is reset"
+ );
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"),
+ "Cache cleaning pref is reset"
+ );
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ "OfflineApps cleaning pref is reset"
+ );
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"),
+ "Downloads cleaning pref is not set"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.prefs.clearUserPref("privacy.clearOnShutdown.downloads");
+ Services.prefs.clearUserPref("privacy.clearOnShutdown.offlineApps");
+ Services.prefs.clearUserPref("privacy.clearOnShutdown.cache");
+ Services.prefs.clearUserPref("privacy.clearOnShutdown.cookies");
+ Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown");
+});
+
+/*
+ * With custom cleaning category already set and SanitizeOnShutdown enabled,
+ * deselecting "deleteOnClose" should not change the state of "alwaysClear".
+ * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box.
+ */
+add_task(async function test_syncWithCustomPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.history", true],
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ let document = gBrowser.contentDocument;
+ let deleteOnCloseBox = document.getElementById("deleteOnClose");
+ let alwaysClearBox = document.getElementById("alwaysClear");
+
+ ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected");
+ ok(alwaysClearBox.checked, "AlwaysClear initial state is selected");
+
+ deleteOnCloseBox.click();
+
+ ok(deleteOnCloseBox.checked, "DeleteOnClose is selected");
+ is(
+ deleteOnCloseBox.checked,
+ alwaysClearBox.checked,
+ "AlwaysClear and deleteOnClose are in the same state, selected"
+ );
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.history"),
+ "History cleaning pref is still set"
+ );
+
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ "Cookie cleaning pref is set"
+ );
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"),
+ "Cache cleaning pref is set"
+ );
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ "OfflineApps cleaning pref is set"
+ );
+
+ deleteOnCloseBox.click();
+
+ ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected");
+ is(
+ !deleteOnCloseBox.checked,
+ alwaysClearBox.checked,
+ "AlwaysClear is not synced with deleteOnClose, only deleteOnClose is deselected"
+ );
+
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ "Cookie cleaning pref is reset"
+ );
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"),
+ "Cache cleaning pref is reset"
+ );
+ ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ "OfflineApps cleaning pref is reset"
+ );
+ ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.history"),
+ "History cleaning pref is still set"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SpecialPowers.popPrefEnv();
+});
+
+/*
+ * Setting/resetting cleaning prefs for cookies, cache, offline apps
+ * and selecting/deselecting the "alwaysClear" Box, also selects/deselects
+ * the "deleteOnClose" box.
+ */
+
+add_task(async function test_syncWithCustomPrefs() {
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ let document = gBrowser.contentDocument;
+ let deleteOnCloseBox = document.getElementById("deleteOnClose");
+ let alwaysClearBox = document.getElementById("alwaysClear");
+
+ ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected");
+ ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cookies", true],
+ ["privacy.clearOnShutdown.cache", true],
+ ["privacy.clearOnShutdown.offlineApps", true],
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ],
+ });
+
+ ok(alwaysClearBox.checked, "AlwaysClear is selected");
+ is(
+ deleteOnCloseBox.checked,
+ alwaysClearBox.checked,
+ "AlwaysClear and deleteOnClose are in the same state, selected"
+ );
+
+ alwaysClearBox.click();
+
+ ok(!alwaysClearBox.checked, "AlwaysClear is deselected");
+ is(
+ deleteOnCloseBox.checked,
+ alwaysClearBox.checked,
+ "AlwaysClear and deleteOnClose are in the same state, deselected"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SpecialPowers.popPrefEnv();
+});
+
+/*
+ * On loading the page, the ClearOnClose box should be set according to the pref selection
+ */
+add_task(async function test_initialState() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cookies", true],
+ ["privacy.clearOnShutdown.cache", true],
+ ["privacy.clearOnShutdown.offlineApps", true],
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ let document = gBrowser.contentDocument;
+ let deleteOnCloseBox = document.getElementById("deleteOnClose");
+
+ ok(
+ deleteOnCloseBox.checked,
+ "DeleteOnClose is set accordingly to the prefs, selected"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cookies", false],
+ ["privacy.clearOnShutdown.cache", false],
+ ["privacy.clearOnShutdown.offlineApps", false],
+ ["privacy.sanitize.sanitizeOnShutdown", true],
+ ["privacy.clearOnShutdown.history", true],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ document = gBrowser.contentDocument;
+ deleteOnCloseBox = document.getElementById("deleteOnClose");
+
+ ok(
+ !deleteOnCloseBox.checked,
+ "DeleteOnClose is set accordingly to the prefs, deselected"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // When private browsing mode autostart is selected, the deleteOnClose Box is selected always
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.clearOnShutdown.cookies", false],
+ ["privacy.clearOnShutdown.cache", false],
+ ["privacy.clearOnShutdown.offlineApps", false],
+ ["privacy.sanitize.sanitizeOnShutdown", false],
+ ["browser.privatebrowsing.autostart", true],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+
+ document = gBrowser.contentDocument;
+ deleteOnCloseBox = document.getElementById("deleteOnClose");
+
+ ok(
+ deleteOnCloseBox.checked,
+ "DeleteOnClose is set accordingly to the private Browsing autostart pref, selected"
+ );
+
+ // Reset history mode
+ let historyMode = document.getElementById("historyMode");
+ historyMode.value = "remember";
+ historyMode.doCommand();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/preferences/tests/browser_privacypane_2.js b/browser/components/preferences/tests/browser_privacypane_2.js
new file mode 100644
index 0000000000..46fb3347c8
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacypane_2.js
@@ -0,0 +1,19 @@
+let rootDir = getRootDirectory(gTestPath);
+let jar = getJar(rootDir);
+if (jar) {
+ let tmpdir = extractJarToTmp(jar);
+ rootDir = "file://" + tmpdir.path + "/";
+}
+/* import-globals-from privacypane_tests_perwindow.js */
+Services.scriptloader.loadSubScript(
+ rootDir + "privacypane_tests_perwindow.js",
+ this
+);
+
+run_test_subset([
+ test_pane_visibility,
+ test_dependent_elements,
+ test_dependent_cookie_elements,
+ test_dependent_clearonclose_elements,
+ test_dependent_prefs,
+]);
diff --git a/browser/components/preferences/tests/browser_privacypane_3.js b/browser/components/preferences/tests/browser_privacypane_3.js
new file mode 100644
index 0000000000..e88a6059eb
--- /dev/null
+++ b/browser/components/preferences/tests/browser_privacypane_3.js
@@ -0,0 +1,21 @@
+let rootDir = getRootDirectory(gTestPath);
+let jar = getJar(rootDir);
+if (jar) {
+ let tmpdir = extractJarToTmp(jar);
+ rootDir = "file://" + tmpdir.path + "/";
+}
+/* import-globals-from privacypane_tests_perwindow.js */
+Services.scriptloader.loadSubScript(
+ rootDir + "privacypane_tests_perwindow.js",
+ this
+);
+
+run_test_subset([
+ test_custom_retention("rememberHistory", "remember"),
+ test_custom_retention("rememberHistory", "custom"),
+ test_custom_retention("rememberForms", "custom"),
+ test_custom_retention("rememberForms", "custom"),
+ test_historymode_retention("remember", "custom"),
+ test_custom_retention("alwaysClear", "remember"),
+ test_custom_retention("alwaysClear", "custom"),
+]);
diff --git a/browser/components/preferences/tests/browser_proxy_backup.js b/browser/components/preferences/tests/browser_proxy_backup.js
new file mode 100644
index 0000000000..fc05e19ada
--- /dev/null
+++ b/browser/components/preferences/tests/browser_proxy_backup.js
@@ -0,0 +1,84 @@
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ // network.proxy.type needs to be backed up and restored because mochitest
+ // changes this setting from the default
+ let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type");
+ registerCleanupFunction(function () {
+ Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType);
+ Services.prefs.clearUserPref("network.proxy.share_proxy_settings");
+ for (let proxyType of ["http", "ssl", "socks"]) {
+ Services.prefs.clearUserPref("network.proxy." + proxyType);
+ Services.prefs.clearUserPref("network.proxy." + proxyType + "_port");
+ if (proxyType == "http") {
+ continue;
+ }
+ Services.prefs.clearUserPref("network.proxy.backup." + proxyType);
+ Services.prefs.clearUserPref(
+ "network.proxy.backup." + proxyType + "_port"
+ );
+ }
+ // On accepting the dialog, we also write TRR values, so we need to clear
+ // them. They are tested separately in browser_privacy_dnsoverhttps.js.
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ });
+
+ let connectionURL =
+ "chrome://browser/content/preferences/dialogs/connection.xhtml";
+
+ // Set a shared proxy and an SSL backup
+ Services.prefs.setIntPref("network.proxy.type", 1);
+ Services.prefs.setBoolPref("network.proxy.share_proxy_settings", true);
+ Services.prefs.setCharPref("network.proxy.http", "example.com");
+ Services.prefs.setIntPref("network.proxy.http_port", 1200);
+ Services.prefs.setCharPref("network.proxy.ssl", "example.com");
+ Services.prefs.setIntPref("network.proxy.ssl_port", 1200);
+ Services.prefs.setCharPref("network.proxy.backup.ssl", "127.0.0.1");
+ Services.prefs.setIntPref("network.proxy.backup.ssl_port", 9050);
+
+ /*
+ The connection dialog alone won't save onaccept since it uses type="child",
+ so it has to be opened as a sub dialog of the main pref tab.
+ Open the main tab here.
+ */
+ open_preferences(async function tabOpened(aContentWindow) {
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences",
+ "about:preferences loaded"
+ );
+ let dialog = await openAndLoadSubDialog(connectionURL);
+ let dialogElement = dialog.document.getElementById("ConnectionsDialog");
+ let dialogClosingPromise = BrowserTestUtils.waitForEvent(
+ dialogElement,
+ "dialogclosing"
+ );
+
+ ok(dialog, "connection window opened");
+ dialogElement.acceptDialog();
+
+ let dialogClosingEvent = await dialogClosingPromise;
+ ok(dialogClosingEvent, "connection window closed");
+
+ // The SSL backup should not be replaced by the shared value
+ is(
+ Services.prefs.getCharPref("network.proxy.backup.ssl"),
+ "127.0.0.1",
+ "Shared proxy backup shouldn't be replaced"
+ );
+ is(
+ Services.prefs.getIntPref("network.proxy.backup.ssl_port"),
+ 9050,
+ "Shared proxy port backup shouldn't be replaced"
+ );
+
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js b/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js
new file mode 100644
index 0000000000..2a7cb2d10e
--- /dev/null
+++ b/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js
@@ -0,0 +1,47 @@
+"use strict";
+
+function switchToCustomHistoryMode(doc) {
+ // Select the last item in the menulist.
+ let menulist = doc.getElementById("historyMode");
+ menulist.focus();
+ EventUtils.sendKey("UP");
+}
+
+function testPrefStateMatchesLockedState() {
+ let win = gBrowser.contentWindow;
+ let doc = win.document;
+ switchToCustomHistoryMode(doc);
+
+ let checkbox = doc.getElementById("alwaysClear");
+ let preference = win.Preferences.get("privacy.sanitize.sanitizeOnShutdown");
+ is(
+ checkbox.disabled,
+ preference.locked,
+ "Always Clear checkbox should be enabled when preference is not locked."
+ );
+
+ Services.prefs.clearUserPref("privacy.history.custom");
+ gBrowser.removeCurrentTab();
+}
+
+add_setup(function () {
+ registerCleanupFunction(function resetPreferences() {
+ Services.prefs.unlockPref("privacy.sanitize.sanitizeOnShutdown");
+ Services.prefs.clearUserPref("privacy.history.custom");
+ });
+});
+
+add_task(async function test_preference_enabled_when_unlocked() {
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ testPrefStateMatchesLockedState();
+});
+
+add_task(async function test_preference_disabled_when_locked() {
+ Services.prefs.lockPref("privacy.sanitize.sanitizeOnShutdown");
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ testPrefStateMatchesLockedState();
+});
diff --git a/browser/components/preferences/tests/browser_searchChangedEngine.js b/browser/components/preferences/tests/browser_searchChangedEngine.js
new file mode 100644
index 0000000000..0882c9775e
--- /dev/null
+++ b/browser/components/preferences/tests/browser_searchChangedEngine.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { SearchUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+SearchTestUtils.init(this);
+
+function findRow(tree, expectedName) {
+ for (let i = 0; i < tree.view.rowCount; i++) {
+ let name = tree.view.getCellText(
+ i,
+ tree.columns.getNamedColumn("engineName")
+ );
+
+ if (name == expectedName) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+add_task(async function test_change_engine() {
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ await SearchTestUtils.installSearchExtension({
+ id: "example@tests.mozilla.org",
+ name: "Example",
+ version: "1.0",
+ keyword: "foo",
+ favicon_url: "img123.png",
+ });
+
+ let tree = doc.querySelector("#engineList");
+
+ let row = findRow(tree, "Example");
+
+ Assert.notEqual(row, -1, "Should have found the entry");
+ Assert.ok(
+ tree.view
+ .getImageSrc(row, tree.columns.getNamedColumn("engineName"))
+ .includes("img123.png"),
+ "Should have the correct image URL"
+ );
+ Assert.equal(
+ tree.view.getCellText(row, tree.columns.getNamedColumn("engineKeyword")),
+ "foo",
+ "Should show the correct keyword"
+ );
+
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ await SearchTestUtils.installSearchExtension({
+ id: "example@tests.mozilla.org",
+ name: "Example 2",
+ version: "2.0",
+ keyword: "bar",
+ favicon_url: "img456.png",
+ });
+ await updatedPromise;
+
+ row = findRow(tree, "Example 2");
+
+ Assert.notEqual(row, -1, "Should have found the updated entry");
+ Assert.ok(
+ tree.view
+ .getImageSrc(row, tree.columns.getNamedColumn("engineName"))
+ .includes("img456.png"),
+ "Should have the correct image URL"
+ );
+ Assert.equal(
+ tree.view.getCellText(row, tree.columns.getNamedColumn("engineKeyword")),
+ "bar",
+ "Should show the correct keyword"
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_searchDefaultEngine.js b/browser/components/preferences/tests/browser_searchDefaultEngine.js
new file mode 100644
index 0000000000..e64f88fab3
--- /dev/null
+++ b/browser/components/preferences/tests/browser_searchDefaultEngine.js
@@ -0,0 +1,372 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+SearchTestUtils.init(this);
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension({
+ name: "engine1",
+ search_url: "https://example.com/engine1",
+ search_url_get_params: "search={searchTerms}",
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: "engine2",
+ search_url: "https://example.com/engine2",
+ search_url_get_params: "search={searchTerms}",
+ });
+
+ const defaultEngine = await Services.search.getDefault();
+ const defaultPrivateEngine = await Services.search.getDefaultPrivate();
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(
+ defaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ defaultPrivateEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ });
+});
+
+add_task(async function test_openWithPrivateDefaultNotEnabledFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", false],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const separateEngineCheckbox = doc.getElementById(
+ "browserSeparateDefaultEngine"
+ );
+ const privateDefaultVbox = doc.getElementById(
+ "browserPrivateEngineSelection"
+ );
+
+ Assert.ok(
+ separateEngineCheckbox.hidden,
+ "Should have hidden the separate search engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should have hidden the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", true]],
+ });
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should have displayed the separate search engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should not have displayed the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", true]],
+ });
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should still be displaying the separate search engine checkbox"
+ );
+ Assert.ok(
+ !privateDefaultVbox.hidden,
+ "Should have displayed the private engine selection box"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_openWithPrivateDefaultEnabledFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const separateEngineCheckbox = doc.getElementById(
+ "browserSeparateDefaultEngine"
+ );
+ const privateDefaultVbox = doc.getElementById(
+ "browserPrivateEngineSelection"
+ );
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should not have hidden the separate search engine checkbox"
+ );
+ Assert.ok(
+ !privateDefaultVbox.hidden,
+ "Should not have hidden the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", false]],
+ });
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should not have hidden the separate search engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should have hidden the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", false]],
+ });
+
+ Assert.ok(
+ separateEngineCheckbox.hidden,
+ "Should have hidden the separate private engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should still be hiding the private engine selection box"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_separatePrivateDefault() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const separateEngineCheckbox = doc.getElementById(
+ "browserSeparateDefaultEngine"
+ );
+ const privateDefaultVbox = doc.getElementById(
+ "browserPrivateEngineSelection"
+ );
+
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should not be displaying the private engine selection box"
+ );
+
+ separateEngineCheckbox.checked = false;
+ separateEngineCheckbox.doCommand();
+
+ Assert.ok(
+ Services.prefs.getBoolPref("browser.search.separatePrivateDefault"),
+ "Should have correctly set the pref"
+ );
+
+ Assert.ok(
+ !privateDefaultVbox.hidden,
+ "Should be displaying the private engine selection box"
+ );
+
+ separateEngineCheckbox.checked = true;
+ separateEngineCheckbox.doCommand();
+
+ Assert.ok(
+ !Services.prefs.getBoolPref("browser.search.separatePrivateDefault"),
+ "Should have correctly turned the pref off"
+ );
+
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should have hidden the private engine selection box"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+async function setDefaultEngine(
+ testPrivate,
+ currentEngineName,
+ expectedEngineName
+) {
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const defaultEngineSelector = doc.getElementById(
+ testPrivate ? "defaultPrivateEngine" : "defaultEngine"
+ );
+
+ Assert.equal(
+ defaultEngineSelector.selectedItem.engine.name,
+ currentEngineName,
+ "Should have the correct engine as default on first open"
+ );
+
+ const popup = defaultEngineSelector.menupopup;
+ const popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ defaultEngineSelector,
+ {},
+ defaultEngineSelector.ownerGlobal
+ );
+ await popupShown;
+
+ const items = Array.from(popup.children);
+ const engine2Item = items.find(
+ item => item.engine.name == expectedEngineName
+ );
+
+ const defaultChanged = SearchTestUtils.promiseSearchNotification(
+ testPrivate ? "engine-default-private" : "engine-default",
+ "browser-search-engine-modified"
+ );
+ // Waiting for popupHiding here seemed to cause a race condition, however
+ // as we're really just interested in the notification, we'll just use
+ // that here.
+ EventUtils.synthesizeMouseAtCenter(engine2Item, {}, engine2Item.ownerGlobal);
+ await defaultChanged;
+
+ const newDefault = testPrivate
+ ? await Services.search.getDefaultPrivate()
+ : await Services.search.getDefault();
+ Assert.equal(
+ newDefault.name,
+ expectedEngineName,
+ "Should have changed the default engine to engine2"
+ );
+}
+
+add_task(async function test_setDefaultEngine() {
+ const engine1 = Services.search.getEngineByName("engine1");
+
+ // Set an initial default so we have a known engine.
+ await Services.search.setDefault(
+ engine1,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+
+ await setDefaultEngine(false, "engine1", "engine2");
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "change_default",
+ value: "user",
+ extra: {
+ prev_id: engine1.telemetryId,
+ new_id: "other-engine2",
+ new_name: "engine2",
+ new_load_path: "[addon]engine2@tests.mozilla.org",
+ new_sub_url: "",
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ let snapshot = await Glean.searchEngineDefault.changed.testGetValue();
+ delete snapshot[0].timestamp;
+ Assert.deepEqual(
+ snapshot[0],
+ {
+ category: "search.engine.default",
+ name: "changed",
+ extra: {
+ change_source: "user",
+ previous_engine_id: engine1.telemetryId,
+ new_engine_id: "other-engine2",
+ new_display_name: "engine2",
+ new_load_path: "[addon]engine2@tests.mozilla.org",
+ new_submission_url: "",
+ },
+ },
+ "Should have received the correct event details"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_setPrivateDefaultEngine() {
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ],
+ });
+
+ const engine2 = Services.search.getEngineByName("engine2");
+
+ // Set an initial default so we have a known engine.
+ await Services.search.setDefaultPrivate(
+ engine2,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+
+ await setDefaultEngine(true, "engine2", "engine1");
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "change_private",
+ value: "user",
+ extra: {
+ prev_id: engine2.telemetryId,
+ new_id: "other-engine1",
+ new_name: "engine1",
+ new_load_path: "[addon]engine1@tests.mozilla.org",
+ new_sub_url: "",
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ let snapshot = await Glean.searchEnginePrivate.changed.testGetValue();
+ delete snapshot[0].timestamp;
+ console.log(snapshot);
+ Assert.deepEqual(
+ snapshot[0],
+ {
+ category: "search.engine.private",
+ name: "changed",
+ extra: {
+ change_source: "user",
+ previous_engine_id: engine2.telemetryId,
+ new_engine_id: "other-engine1",
+ new_display_name: "engine1",
+ new_load_path: "[addon]engine1@tests.mozilla.org",
+ new_submission_url: "",
+ },
+ },
+ "Should have received the correct event details"
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_searchFindMoreLink.js b/browser/components/preferences/tests/browser_searchFindMoreLink.js
new file mode 100644
index 0000000000..92864c9f54
--- /dev/null
+++ b/browser/components/preferences/tests/browser_searchFindMoreLink.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "https://example.org/";
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.searchEnginesURL", TEST_URL]],
+ });
+});
+
+add_task(async function test_click_find_more_link() {
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ gBrowser.selectedBrowser.contentDocument
+ .getElementById("addEngines")
+ .scrollIntoView();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#addEngines",
+ {},
+ gBrowser.selectedBrowser.browsingContext
+ );
+
+ let tab = await promiseNewTab;
+ Assert.equal(
+ tab.linkedBrowser.documentURI.spec,
+ TEST_URL,
+ "Should have loaded the expected page"
+ );
+
+ // Close both tabs.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_searchRestoreDefaults.js b/browser/components/preferences/tests/browser_searchRestoreDefaults.js
new file mode 100644
index 0000000000..80588d0cce
--- /dev/null
+++ b/browser/components/preferences/tests/browser_searchRestoreDefaults.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { SearchUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchUtils.sys.mjs"
+);
+add_task(async function test_restore_functionality() {
+ // Ensure no engines are hidden to begin with.
+ for (let engine of await Services.search.getAppProvidedEngines()) {
+ if (engine.hidden) {
+ engine.hidden = false;
+ }
+ }
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines");
+
+ Assert.ok(
+ restoreDefaultsButton.disabled,
+ "Should have disabled the restore default search engines button on open"
+ );
+
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+
+ let tree = doc.querySelector("#engineList");
+ // Check for default search engines to be displayed in the engineList
+ let defaultEngines = await Services.search.getAppProvidedEngines();
+ for (let i = 0; i < defaultEngines.length; i++) {
+ let cellName = tree.view.getCellText(
+ i,
+ tree.columns.getNamedColumn("engineName")
+ );
+ if (cellName == "DuckDuckGo") {
+ tree.view.selection.select(i);
+ break;
+ }
+ }
+ doc.getElementById("removeEngineButton").click();
+ await updatedPromise;
+
+ let engine = await Services.search.getEngineByName("DuckDuckGo");
+
+ Assert.ok(engine.hidden, "Should have hidden the engine");
+ Assert.ok(
+ !restoreDefaultsButton.disabled,
+ "Should have enabled the restore default search engines button"
+ );
+
+ updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ restoreDefaultsButton.click();
+ await updatedPromise;
+ // Let the stack unwind so that the restore defaults button can update its
+ // state.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ Assert.ok(!engine.hidden, "Should have re-enabled the disabled engine");
+ Assert.ok(
+ restoreDefaultsButton.disabled,
+ "Should have disabled the restore default search engines button after use"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_restoreEnabledOnOpenWithEngineHidden() {
+ let engine = await Services.search.getEngineByName("DuckDuckGo");
+ engine.hidden = true;
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines");
+
+ Assert.ok(
+ !restoreDefaultsButton.disabled,
+ "Should have enabled the restore default search engines button on open"
+ );
+
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ restoreDefaultsButton.click();
+ await updatedPromise;
+ // Let the stack unwind so that the restore defaults button can update its
+ // state.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ Assert.ok(!engine.hidden, "Should have re-enabled the disabled engine");
+ Assert.ok(
+ restoreDefaultsButton.disabled,
+ "Should have disabled the restore default search engines button after use"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// This removes the last two engines and then the remaining engines from top to
+// bottom, and then it restores the default engines. See bug 1681818.
+add_task(async function test_removeOutOfOrder() {
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines");
+ Assert.ok(
+ restoreDefaultsButton.disabled,
+ "The restore-defaults button is disabled initially"
+ );
+
+ let tree = doc.querySelector("#engineList");
+ let removeEngineButton = doc.getElementById("removeEngineButton");
+ removeEngineButton.scrollIntoView();
+
+ let defaultEngines = await Services.search.getAppProvidedEngines();
+
+ // Remove the last two engines. After each removal, the selection should move
+ // to the first local shortcut.
+ for (let i = 0; i < 2; i++) {
+ tree.view.selection.select(defaultEngines.length - i - 1);
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ removeEngineButton.click();
+ await updatedPromise;
+ Assert.ok(
+ removeEngineButton.disabled,
+ "The remove-engine button is disabled because a local shortcut is selected"
+ );
+ Assert.ok(
+ !restoreDefaultsButton.disabled,
+ "The restore-defaults button is enabled after removing an engine"
+ );
+ }
+
+ // Remove the remaining engines from top to bottom except for the default engine
+ // which can't be removed.
+ for (let i = 0; i < defaultEngines.length - 3; i++) {
+ tree.view.selection.select(0);
+
+ if (defaultEngines[0].name == Services.search.defaultEngine.name) {
+ tree.view.selection.select(1);
+ }
+
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ removeEngineButton.click();
+ await updatedPromise;
+ Assert.ok(
+ !restoreDefaultsButton.disabled,
+ "The restore-defaults button is enabled after removing an engine"
+ );
+ }
+ Assert.ok(
+ removeEngineButton.disabled,
+ "The remove-engine button is disabled because only one engine remains"
+ );
+
+ // Click the restore-defaults button.
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ restoreDefaultsButton.click();
+ await updatedPromise;
+
+ // Wait for the restore-defaults button to update its state.
+ await TestUtils.waitForCondition(
+ () => restoreDefaultsButton.disabled,
+ "Waiting for the restore-defaults button to become disabled"
+ );
+
+ Assert.ok(
+ restoreDefaultsButton.disabled,
+ "The restore-defaults button is disabled after restoring defaults"
+ );
+ Assert.equal(
+ tree.view.rowCount,
+ defaultEngines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length,
+ "All engines are restored"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_removeAndRestoreMultiple() {
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines");
+ let tree = doc.querySelector("#engineList");
+ let removeEngineButton = doc.getElementById("removeEngineButton");
+ removeEngineButton.scrollIntoView();
+
+ let defaultEngines = await Services.search.getAppProvidedEngines();
+
+ // Remove the second and fourth engines.
+ for (let i = 0; i < 2; i++) {
+ tree.view.selection.select(i * 2 + 1);
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ removeEngineButton.click();
+ await updatedPromise;
+ }
+
+ // Click the restore-defaults button.
+ let updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ restoreDefaultsButton.click();
+ await updatedPromise;
+
+ // Remove the third engine.
+ tree.view.selection.select(3);
+ updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ removeEngineButton.click();
+ await updatedPromise;
+
+ // Now restore again.
+ updatedPromise = SearchTestUtils.promiseSearchNotification(
+ SearchUtils.MODIFIED_TYPE.CHANGED,
+ SearchUtils.TOPIC_ENGINE_MODIFIED
+ );
+ restoreDefaultsButton.click();
+ await updatedPromise;
+
+ // Wait for the restore-defaults button to update its state.
+ await TestUtils.waitForCondition(
+ () => restoreDefaultsButton.disabled,
+ "Waiting for the restore-defaults button to become disabled"
+ );
+
+ Assert.equal(
+ tree.view.rowCount,
+ defaultEngines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length,
+ "Should have the correct amount of engines"
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_searchScroll.js b/browser/components/preferences/tests/browser_searchScroll.js
new file mode 100644
index 0000000000..ef3af646e9
--- /dev/null
+++ b/browser/components/preferences/tests/browser_searchScroll.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { SearchUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+SearchTestUtils.init(this);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.w3c_touch_events.enabled", 0]],
+ });
+});
+
+add_task(async function test_scroll() {
+ info("Open preferences page for search");
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const tree = doc.querySelector("#engineList");
+
+ info("Add engines to make the tree scrollable");
+ for (let i = 0, n = parseInt(tree.getAttribute("rows")); i < n; i++) {
+ let extension = await SearchTestUtils.installSearchExtension({
+ id: `${i}@tests.mozilla.org`,
+ name: `example${i}`,
+ version: "1.0",
+ keyword: `example${i}`,
+ });
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ }
+
+ info("Make tree element move into viewport");
+ const mainContent = doc.querySelector(".main-content");
+ const readyForScrollIntoView = new Promise(r => {
+ mainContent.addEventListener("scroll", r, { once: true });
+ });
+ tree.scrollIntoView();
+ await readyForScrollIntoView;
+
+ const previousScroll = mainContent.scrollTop;
+
+ await promiseMoveMouseAndScrollWheelOver(tree, 1, 1, false);
+
+ Assert.equal(
+ previousScroll,
+ mainContent.scrollTop,
+ "Container element does not scroll"
+ );
+
+ info("Clean up");
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js
new file mode 100644
index 0000000000..33ab63e8bf
--- /dev/null
+++ b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const MAIN_PREF = "browser.search.suggest.enabled";
+const URLBAR_PREF = "browser.urlbar.suggest.searches";
+const FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst";
+const FIRST_CHECKBOX_ID = "showSearchSuggestionsFirstCheckbox";
+
+add_setup(async function () {
+ // Make sure the main and urlbar suggestion prefs are enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [MAIN_PREF, true],
+ [URLBAR_PREF, true],
+ ],
+ });
+});
+
+// Open preferences with search suggestions shown first (the default).
+add_task(async function openWithSearchSuggestionsShownFirst() {
+ // Initially the pref should be true so search suggestions are shown first.
+ Assert.ok(
+ Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should be true initially"
+ );
+
+ // Open preferences. The checkbox should be checked.
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let checkbox = doc.getElementById(FIRST_CHECKBOX_ID);
+ Assert.ok(checkbox.checked, "Checkbox should be checked");
+ Assert.ok(!checkbox.disabled, "Checkbox should be enabled");
+
+ // Uncheck the checkbox.
+ checkbox.checked = false;
+ checkbox.doCommand();
+
+ // The pref should now be false so that history is shown first.
+ Assert.ok(
+ !Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should now be false to show history first"
+ );
+
+ // Make sure the checkbox state didn't change.
+ Assert.ok(!checkbox.checked, "Checkbox should remain unchecked");
+ Assert.ok(!checkbox.disabled, "Checkbox should remain enabled");
+
+ // Clear the pref.
+ Services.prefs.clearUserPref(FIRST_PREF);
+
+ // The checkbox should have become checked again.
+ Assert.ok(
+ checkbox.checked,
+ "Checkbox should become checked after clearing pref"
+ );
+ Assert.ok(
+ !checkbox.disabled,
+ "Checkbox should remain enabled after clearing pref"
+ );
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+});
+
+// Open preferences with history shown first.
+add_task(async function openWithHistoryShownFirst() {
+ // Set the pref to show history first.
+ Services.prefs.setBoolPref(FIRST_PREF, false);
+
+ // Open preferences. The checkbox should be unchecked.
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let checkbox = doc.getElementById(FIRST_CHECKBOX_ID);
+ Assert.ok(!checkbox.checked, "Checkbox should be unchecked");
+ Assert.ok(!checkbox.disabled, "Checkbox should be enabled");
+
+ // Check the checkbox.
+ checkbox.checked = true;
+ checkbox.doCommand();
+
+ // Make sure the checkbox state didn't change.
+ Assert.ok(checkbox.checked, "Checkbox should remain checked");
+ Assert.ok(!checkbox.disabled, "Checkbox should remain enabled");
+
+ // The pref should now be true so that search suggestions are shown first.
+ Assert.ok(
+ Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should now be true to show search suggestions first"
+ );
+
+ // Set the pref to false again.
+ Services.prefs.setBoolPref(FIRST_PREF, false);
+
+ // The checkbox should have become unchecked again.
+ Assert.ok(
+ !checkbox.checked,
+ "Checkbox should become unchecked after setting pref to false"
+ );
+ Assert.ok(
+ !checkbox.disabled,
+ "Checkbox should remain enabled after setting pref to false"
+ );
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref(FIRST_PREF);
+});
+
+// Checks how the show-suggestions-first pref and checkbox reacts to updates to
+// URLBAR_PREF and MAIN_PREF.
+add_task(async function superprefInteraction() {
+ // Initially the pref should be true so search suggestions are shown first.
+ Assert.ok(
+ Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should be true initially"
+ );
+
+ // Open preferences. The checkbox should be checked.
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let checkbox = doc.getElementById(FIRST_CHECKBOX_ID);
+ Assert.ok(checkbox.checked, "Checkbox should be checked");
+ Assert.ok(!checkbox.disabled, "Checkbox should be enabled");
+
+ // Two superior prefs control the show-suggestion-first pref: URLBAR_PREF and
+ // MAIN_PREF. Toggle each and make sure the show-suggestion-first checkbox
+ // reacts appropriately.
+ for (let superiorPref of [URLBAR_PREF, MAIN_PREF]) {
+ info(`Testing superior pref ${superiorPref}`);
+
+ // Set the superior pref to false.
+ Services.prefs.setBoolPref(superiorPref, false);
+
+ // The pref should remain true.
+ Assert.ok(
+ Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should remain true"
+ );
+
+ // The checkbox should have become unchecked and disabled.
+ Assert.ok(
+ !checkbox.checked,
+ "Checkbox should become unchecked after disabling urlbar suggestions"
+ );
+ Assert.ok(
+ checkbox.disabled,
+ "Checkbox should become disabled after disabling urlbar suggestions"
+ );
+
+ // Set the superior pref to true.
+ Services.prefs.setBoolPref(superiorPref, true);
+
+ // The pref should remain true.
+ Assert.ok(
+ Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should remain true"
+ );
+
+ // The checkbox should have become checked and enabled again.
+ Assert.ok(
+ checkbox.checked,
+ "Checkbox should become checked after re-enabling urlbar suggestions"
+ );
+ Assert.ok(
+ !checkbox.disabled,
+ "Checkbox should become enabled after re-enabling urlbar suggestions"
+ );
+
+ // Set the pref to false.
+ Services.prefs.setBoolPref(FIRST_PREF, false);
+
+ // The checkbox should have become unchecked.
+ Assert.ok(
+ !checkbox.checked,
+ "Checkbox should become unchecked after setting pref to false"
+ );
+ Assert.ok(
+ !checkbox.disabled,
+ "Checkbox should remain enabled after setting pref to false"
+ );
+
+ // Set the superior pref to false again.
+ Services.prefs.setBoolPref(superiorPref, false);
+
+ // The pref should remain false.
+ Assert.ok(
+ !Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should remain false"
+ );
+
+ // The checkbox should remain unchecked and become disabled.
+ Assert.ok(
+ !checkbox.checked,
+ "Checkbox should remain unchecked after disabling urlbar suggestions"
+ );
+ Assert.ok(
+ checkbox.disabled,
+ "Checkbox should become disabled after disabling urlbar suggestions"
+ );
+
+ // Set the superior pref to true.
+ Services.prefs.setBoolPref(superiorPref, true);
+
+ // The pref should remain false.
+ Assert.ok(
+ !Services.prefs.getBoolPref(FIRST_PREF),
+ "Pref should remain false"
+ );
+
+ // The checkbox should remain unchecked and become enabled.
+ Assert.ok(
+ !checkbox.checked,
+ "Checkbox should remain unchecked after re-enabling urlbar suggestions"
+ );
+ Assert.ok(
+ !checkbox.disabled,
+ "Checkbox should become enabled after re-enabling urlbar suggestions"
+ );
+
+ // Finally, set the pref back to true.
+ Services.prefs.setBoolPref(FIRST_PREF, true);
+
+ // The checkbox should have become checked.
+ Assert.ok(
+ checkbox.checked,
+ "Checkbox should become checked after setting pref back to true"
+ );
+ Assert.ok(
+ !checkbox.disabled,
+ "Checkbox should remain enabled after setting pref back to true"
+ );
+ }
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+
+ Services.prefs.clearUserPref(FIRST_PREF);
+ Services.prefs.clearUserPref(URLBAR_PREF);
+ Services.prefs.clearUserPref(MAIN_PREF);
+});
diff --git a/browser/components/preferences/tests/browser_search_no_results_change_category.js b/browser/components/preferences/tests/browser_search_no_results_change_category.js
new file mode 100644
index 0000000000..131492632e
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_no_results_change_category.js
@@ -0,0 +1,44 @@
+"use strict";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+ let query = "ffff____noresults____ffff";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ let noResultsEl = gBrowser.contentDocument.querySelector(
+ "#no-results-message"
+ );
+ is_element_visible(
+ noResultsEl,
+ "Should be reporting no results for this query"
+ );
+
+ await gBrowser.contentWindow.gotoPref("panePrivacy");
+ is_element_hidden(
+ noResultsEl,
+ "Should not be showing the 'no results' message after selecting a category"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_searchTerms.js b/browser/components/preferences/tests/browser_search_searchTerms.js
new file mode 100644
index 0000000000..0af0591355
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_searchTerms.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ Tests the showSearchTerms option on the about:preferences#search page.
+*/
+
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+const GROUP_ID = "searchbarGroup";
+const CHECKBOX_ID = "searchShowSearchTermCheckbox";
+const PREF_SEARCHTERMS = "browser.urlbar.showSearchTerms.enabled";
+const PREF_FEATUREGATE = "browser.urlbar.showSearchTerms.featureGate";
+
+/*
+ If Nimbus experiment is enabled, check option visibility.
+*/
+add_task(async function showSearchTermsVisibility_experiment_beforeOpen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_FEATUREGATE, false]],
+ });
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ showSearchTermsFeatureGate: true,
+ },
+ callback: async () => {
+ await openPreferencesViaOpenPreferencesAPI("search", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let container = doc.getElementById(CHECKBOX_ID);
+ Assert.ok(
+ BrowserTestUtils.is_visible(container),
+ "The option box is visible"
+ );
+ gBrowser.removeCurrentTab();
+ },
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+/*
+ If Nimbus experiment is not enabled initially but eventually enabled,
+ check option visibility on Preferences page.
+*/
+add_task(async function showSearchTermsVisibility_experiment_afterOpen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_FEATUREGATE, false]],
+ });
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let container = doc.getElementById(CHECKBOX_ID);
+ Assert.ok(
+ BrowserTestUtils.is_hidden(container),
+ "The option box is initially hidden."
+ );
+
+ // Install experiment.
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ showSearchTermsFeatureGate: true,
+ },
+ callback: async () => {
+ Assert.ok(
+ BrowserTestUtils.is_visible(container),
+ "The option box is visible"
+ );
+ },
+ });
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(container),
+ "The option box is hidden again after the experiment is uninstalled."
+ );
+
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+/*
+ Check using the checkbox modifies the preference.
+*/
+add_task(async function showSearchTerms_checkbox() {
+ // Enable the feature.
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_FEATUREGATE, true]],
+ });
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ doc.getElementById(GROUP_ID).scrollIntoView();
+
+ let option = doc.getElementById(CHECKBOX_ID);
+
+ // Evaluate checkbox pref is true.
+ Assert.ok(option.checked, "Option box should be checked.");
+
+ // Evaluate checkbox when pref is false.
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_SEARCHTERMS, false]],
+ });
+ Assert.ok(!option.checked, "Option box should not be checked.");
+ await SpecialPowers.popPrefEnv();
+
+ // Evaluate pref when checkbox is un-checked.
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + CHECKBOX_ID,
+ {},
+ gBrowser.selectedBrowser
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_SEARCHTERMS),
+ false,
+ "Preference should be false if un-checked."
+ );
+
+ // Evaluate pref when checkbox is checked.
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + CHECKBOX_ID,
+ {},
+ gBrowser.selectedBrowser
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_SEARCHTERMS),
+ true,
+ "Preference should be true if checked."
+ );
+
+ // Clean-up.
+ Services.prefs.clearUserPref(PREF_SEARCHTERMS);
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+/*
+ When loading the search preferences panel, the
+ showSearchTerms checkbox should be disabled if
+ the search bar is enabled.
+*/
+add_task(async function showSearchTerms_and_searchBar_preference_load() {
+ // Enable the feature.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_FEATUREGATE, true],
+ ["browser.search.widget.inNavBar", true],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ let checkbox = doc.getElementById(CHECKBOX_ID);
+ Assert.ok(
+ checkbox.disabled,
+ "showSearchTerms checkbox should be disabled when search bar is enabled."
+ );
+
+ // Clean-up.
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+/*
+ If the search bar is enabled while the search
+ preferences panel is open, the showSearchTerms
+ checkbox should not be clickable.
+*/
+add_task(async function showSearchTerms_and_searchBar_preference_change() {
+ // Enable the feature.
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_FEATUREGATE, true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ let checkbox = doc.getElementById(CHECKBOX_ID);
+ Assert.ok(!checkbox.disabled, "showSearchTerms checkbox should be enabled.");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", true]],
+ });
+ Assert.ok(
+ checkbox.disabled,
+ "showSearchTerms checkbox should be disabled when search bar is enabled."
+ );
+
+ // Clean-up.
+ await SpecialPowers.popPrefEnv();
+ Assert.ok(!checkbox.disabled, "showSearchTerms checkbox should be enabled.");
+
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js b/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js
new file mode 100644
index 0000000000..8235f8dd19
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js
@@ -0,0 +1,39 @@
+"use strict";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+add_task(async function test_show_search_term_tooltip_in_subdialog() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let keyword = "organization";
+ await runSearchInput(keyword);
+
+ let formAutofillGroupBox = gBrowser.contentDocument.getElementById(
+ "formAutofillGroupBox"
+ );
+ let savedAddressesButton =
+ formAutofillGroupBox.querySelector(".accessory-button");
+
+ info("Clicking saved addresses button to open subdialog");
+ savedAddressesButton.click();
+ info("Waiting for addresses subdialog to appear");
+ await BrowserTestUtils.waitForCondition(() => {
+ let dialogBox = gBrowser.contentDocument.querySelector(".dialogBox");
+ return !!dialogBox;
+ });
+ let tooltip = gBrowser.contentDocument.querySelector(".search-tooltip");
+
+ is_element_visible(
+ tooltip,
+ "Tooltip with search term should be visible in subdialog"
+ );
+ is(tooltip.textContent, keyword, "Tooltip should have correct search term");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js
new file mode 100644
index 0000000000..e676de0a6c
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js
@@ -0,0 +1,48 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Set Home Page" subdialog.
+ */
+add_task(async function () {
+ // Set custom URL so bookmark button will be shown on the page (otherwise it is hidden)
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.startup.homepage", "about:robots"],
+ ["browser.startup.page", 1],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true });
+
+ // Wait for Activity Stream to add its panels
+ await BrowserTestUtils.waitForCondition(() =>
+ SpecialPowers.spawn(
+ gBrowser.selectedTab.linkedBrowser,
+ [],
+ () => !!content.document.getElementById("homeContentsGroup")
+ )
+ );
+
+ await evaluateSearchResults("Set Home Page", "homepageGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Languages" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("Choose languages", "languagesGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js
new file mode 100644
index 0000000000..6ff05dee29
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js
@@ -0,0 +1,36 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Saved Logins" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("sites are stored", "passwordsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Exceptions - Enhanced Tracking Protection" subdialog:
+ * "You can specify which websites have Enhanced Tracking Protection turned off." #permissions-exceptions-manage-etp-desc
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults(
+ "Enhanced Tracking Protection turned off",
+ "trackingGroup"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js
new file mode 100644
index 0000000000..45f8774d73
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js
@@ -0,0 +1,35 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Allowed Sites - Add-ons Installation" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("allowed to install add-ons", "permissionsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Certificate Manager" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults(
+ "identify these certificate authorities",
+ "certSelection"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js
new file mode 100644
index 0000000000..9cfcb51f1b
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js
@@ -0,0 +1,39 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Update History" subdialog.
+ */
+add_task(async function () {
+ // The updates panel is disabled in MSIX builds.
+ if (
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId")
+ ) {
+ return;
+ }
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("updates have been installed", "updateApp");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Location Permissions" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("location permissions", "permissionsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js
new file mode 100644
index 0000000000..9a4ae09696
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js
@@ -0,0 +1,46 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+requestLongerTimeout(2);
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Fonts" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ // Oh, Canada:
+ await evaluateSearchResults("Unified Canadian Syllabary", "fontsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Colors" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("Link Colors", "colorsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Exceptions - Saved Logins" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("sites will not be saved", "passwordsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js
new file mode 100644
index 0000000000..1091dd2dd4
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js
@@ -0,0 +1,35 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Block Lists" subdialog.
+ */
+add_task(async function () {
+ async function doTest() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("block online trackers", "trackingGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ await doTest();
+});
+
+/**
+ * Test for searching for the "Allowed Sites - Pop-ups" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("open pop-up windows", "permissionsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js
new file mode 100644
index 0000000000..1c4a923f06
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js
@@ -0,0 +1,34 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+requestLongerTimeout(2);
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Device Manager" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("Security Modules and Devices", "certSelection");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Connection Settings" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("Use system proxy settings", "connectionGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js
new file mode 100644
index 0000000000..9827e89239
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js
@@ -0,0 +1,45 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+requestLongerTimeout(2);
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Camera Permissions" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("camera permissions", "permissionsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Microphone Permissions" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("microphone permissions", "permissionsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for searching for the "Notification Permissions" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("notification permissions", "permissionsGroup");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js
new file mode 100644
index 0000000000..b7ca239358
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js
@@ -0,0 +1,45 @@
+/*
+ * This file contains tests for the Preferences search bar.
+ */
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test for searching for the "Settings - Site Data" subdialog.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("cookies", ["siteDataGroup", "trackingGroup"]);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("site data", ["siteDataGroup"]);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("cache", ["siteDataGroup"]);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ await evaluateSearchResults("cross-site", ["trackingGroup"]);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_within_preferences_1.js b/browser/components/preferences/tests/browser_search_within_preferences_1.js
new file mode 100644
index 0000000000..504191ecb5
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_within_preferences_1.js
@@ -0,0 +1,344 @@
+"use strict";
+/**
+ * This file contains tests for the Preferences search bar.
+ */
+
+requestLongerTimeout(6);
+
+/**
+ * Tests to see if search bar is being shown when pref is turned on
+ */
+add_task(async function show_search_bar_when_pref_is_enabled() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+ is_element_visible(searchInput, "Search box should be shown");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for "Search Result" panel.
+ * After it runs a search, it tests if the "Search Results" panel is the only selected category.
+ * The search is then cleared, it then tests if the "General" panel is the only selected category.
+ */
+add_task(async function show_search_results_pane_only_then_revert_to_general() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ // Performs search
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let query = "password";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ let categoriesList = gBrowser.contentDocument.getElementById("categories");
+
+ for (let i = 0; i < categoriesList.childElementCount; i++) {
+ let child = categoriesList.itemChildren[i];
+ is(child.selected, false, "No other panel should be selected");
+ }
+ // Takes search off
+ searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == ""
+ );
+ let count = query.length;
+ while (count--) {
+ EventUtils.sendKey("BACK_SPACE");
+ }
+ await searchCompletedPromise;
+
+ // Checks if back to generalPane
+ for (let i = 0; i < categoriesList.childElementCount; i++) {
+ let child = categoriesList.itemChildren[i];
+ if (child.id == "category-general") {
+ is(child.selected, true, "General panel should be selected");
+ } else if (child.id) {
+ is(child.selected, false, "No other panel should be selected");
+ }
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for "password" case. When we search "password", it should show the "passwordGroup"
+ */
+add_task(async function search_for_password_show_passwordGroup() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ // Performs search
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let query = "password";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane");
+
+ for (let i = 0; i < mainPrefTag.childElementCount; i++) {
+ let child = mainPrefTag.children[i];
+ if (
+ child.id == "passwordsGroup" ||
+ child.id == "weavePrefsDeck" ||
+ child.id == "header-searchResults" ||
+ child.id == "certSelection" ||
+ child.id == "connectionGroup" ||
+ child.id == "dataMigrationGroup"
+ ) {
+ is_element_visible(child, "Should be in search results");
+ } else if (child.id) {
+ is_element_hidden(child, "Should not be in search results");
+ }
+ }
+
+ // Takes search off
+ searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == ""
+ );
+ let count = query.length;
+ while (count--) {
+ EventUtils.sendKey("BACK_SPACE");
+ }
+ await searchCompletedPromise;
+
+ let expectedChildren = [
+ "paneGeneral",
+ "startupGroup",
+ "languagesGroup",
+ "webAppearanceGroup",
+ "colorsGroup",
+ "fontsGroup",
+ "zoomGroup",
+ "downloadsGroup",
+ "applicationsGroup",
+ "drmGroup",
+ "browsingGroup",
+ "performanceGroup",
+ "connectionGroup",
+ "generalCategory",
+ "languageAndAppearanceCategory",
+ "filesAndApplicationsCategory",
+ "performanceCategory",
+ "browsingCategory",
+ "networkProxyCategory",
+ "dataMigrationGroup",
+ "translationsGroup",
+ ];
+ // Only visible for non-MSIX builds
+ if (
+ AppConstants.platform !== "win" ||
+ !Services.sysinfo.getProperty("hasWinPackageId", false)
+ ) {
+ expectedChildren.push("updatesCategory");
+ expectedChildren.push("updateApp");
+ }
+ // Checks if back to generalPane
+ for (let i = 0; i < mainPrefTag.childElementCount; i++) {
+ let child = mainPrefTag.children[i];
+ if (expectedChildren.includes(child.id)) {
+ is_element_visible(child, `Should be in general tab: ${child.id}`);
+ } else if (child.id) {
+ is_element_hidden(child, `Should not be in general tab: ${child.id}`);
+ }
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for if nothing is found
+ */
+add_task(async function search_with_nothing_found() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ let noResultsEl = gBrowser.contentDocument.querySelector(
+ "#no-results-message"
+ );
+ let sorryMsgQueryEl = gBrowser.contentDocument.getElementById(
+ "sorry-message-query"
+ );
+
+ is_element_hidden(noResultsEl, "Should not be in search results yet");
+
+ // Performs search
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let query = "coach";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ is_element_visible(noResultsEl, "Should be in search results");
+ is(
+ sorryMsgQueryEl.textContent,
+ query,
+ "sorry-message-query should contain the query"
+ );
+
+ // Takes search off
+ searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == ""
+ );
+ let count = query.length;
+ while (count--) {
+ EventUtils.sendKey("BACK_SPACE");
+ }
+ await searchCompletedPromise;
+
+ is_element_hidden(noResultsEl, "Should not be in search results");
+ is(
+ sorryMsgQueryEl.textContent.length,
+ 0,
+ "sorry-message-query should be empty"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for if we go back to general tab after search case
+ */
+add_task(async function exiting_search_reverts_to_general_pane() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let generalPane = gBrowser.contentDocument.getElementById("generalCategory");
+
+ is_element_hidden(generalPane, "Should not be in general");
+
+ // Performs search
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let query = "password";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ // Takes search off
+ searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == ""
+ );
+ let count = query.length;
+ while (count--) {
+ EventUtils.sendKey("BACK_SPACE");
+ }
+ await searchCompletedPromise;
+
+ // Checks if back to normal
+ is_element_visible(generalPane, "Should be in generalPane");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for if we go to another tab after searching
+ */
+add_task(async function changing_tabs_after_searching() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let query = "permission";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ // Search header should be shown for the permissions group
+ let permissionsSearchHeader = gBrowser.contentDocument.querySelector(
+ "#permissionsGroup .search-header"
+ );
+ is(
+ permissionsSearchHeader.hidden,
+ false,
+ "Permissions search-header should be visible"
+ );
+
+ let privacyCategory =
+ gBrowser.contentDocument.getElementById("category-privacy");
+ privacyCategory.click();
+ is(searchInput.value, "", "search input should be empty");
+ let categoriesList = gBrowser.contentDocument.getElementById("categories");
+ for (let i = 0; i < categoriesList.childElementCount; i++) {
+ let child = categoriesList.itemChildren[i];
+ if (child.id == "category-privacy") {
+ is(child.selected, true, "Privacy panel should be selected");
+ } else if (child.id) {
+ is(child.selected, false, "No other panel should be selected");
+ }
+ }
+
+ // Search header should now be hidden when viewing the permissions group not through a search
+ is(
+ permissionsSearchHeader.hidden,
+ true,
+ "Permissions search-header should be hidden"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_within_preferences_2.js b/browser/components/preferences/tests/browser_search_within_preferences_2.js
new file mode 100644
index 0000000000..6de068fbe4
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_within_preferences_2.js
@@ -0,0 +1,180 @@
+"use strict";
+/**
+ * This file contains tests for the Preferences search bar.
+ */
+
+/**
+ * Enabling searching functionality. Will display search bar from this testcase forward.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.preferences.search", true]],
+ });
+});
+
+/**
+ * Test that we only search the selected child of a XUL deck.
+ * When we search "Remove Account",
+ * it should not show the "Remove Account" button if the Firefox account is not logged in yet.
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("paneSync", { leaveOpen: true });
+
+ let weavePrefsDeck =
+ gBrowser.contentDocument.getElementById("weavePrefsDeck");
+ is(
+ weavePrefsDeck.selectedIndex,
+ 0,
+ "Should select the #noFxaAccount child node"
+ );
+
+ // Performs search.
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let query = "Sync";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane");
+ for (let i = 0; i < mainPrefTag.childElementCount; i++) {
+ let child = mainPrefTag.children[i];
+ if (child.id == "header-searchResults" || child.id == "weavePrefsDeck") {
+ is_element_visible(child, "Should be in search results");
+ } else if (child.id) {
+ is_element_hidden(child, "Should not be in search results");
+ }
+ }
+
+ // Ensure the "Remove Account" button exists in the hidden child of the <xul:deck>.
+ let unlinkFxaAccount = weavePrefsDeck.children[1].querySelector(
+ "#unverifiedUnlinkFxaAccount"
+ );
+ is(
+ unlinkFxaAccount.label,
+ "Remove Account",
+ "The Remove Account button should exist"
+ );
+
+ // Performs search.
+ searchInput.focus();
+ query = "Remove Account";
+ searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ let noResultsEl = gBrowser.contentDocument.querySelector(
+ "#no-results-message"
+ );
+ is_element_visible(noResultsEl, "Should be reporting no results");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test that we search using `search-l10n-ids`.
+ *
+ * The test uses element `showUpdateHistory` and
+ * l10n id `language-and-appearance-header` and expects the element
+ * to be matched on the first word from the l10n id value ("Language" in en-US).
+ */
+add_task(async function () {
+ let l10nId = "language-and-appearance-header";
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ // First, lets make sure that the element is not matched without
+ // `search-l10n-ids`.
+ {
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+ let suhElem = gBrowser.contentDocument.getElementById("showUpdateHistory");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ ok(
+ !suhElem.getAttribute("search-l10n-ids").includes(l10nId),
+ "showUpdateHistory element should not contain the l10n id here."
+ );
+
+ let query = "Language";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ is_element_hidden(
+ suhElem,
+ "showUpdateHistory should not be in search results"
+ );
+ }
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Now, let's add the l10n id to the element and perform the same search again.
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ {
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let suhElem = gBrowser.contentDocument.getElementById("showUpdateHistory");
+ suhElem.setAttribute("search-l10n-ids", l10nId);
+
+ let query = "Language";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ if (
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId")
+ ) {
+ is_element_hidden(
+ suhElem,
+ "showUpdateHistory should not be in search results"
+ );
+ } else {
+ is_element_visible(
+ suhElem,
+ "showUpdateHistory should be in search results"
+ );
+ }
+ }
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_search_within_preferences_command.js b/browser/components/preferences/tests/browser_search_within_preferences_command.js
new file mode 100644
index 0000000000..736493b418
--- /dev/null
+++ b/browser/components/preferences/tests/browser_search_within_preferences_command.js
@@ -0,0 +1,45 @@
+"use strict";
+
+/**
+ * Test for "command" event on search input (when user clicks the x button)
+ */
+add_task(async function () {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ let generalPane = gBrowser.contentDocument.getElementById("generalCategory");
+
+ is_element_hidden(generalPane, "Should not be in general");
+
+ // Performs search
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+ is(
+ searchInput,
+ gBrowser.contentDocument.activeElement.closest("#searchInput"),
+ "Search input should be focused when visiting preferences"
+ );
+
+ let query = "x";
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == query
+ );
+ EventUtils.sendString(query);
+ await searchCompletedPromise;
+
+ is_element_hidden(generalPane, "Should not be in generalPane");
+
+ // Takes search off with "command"
+ searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == ""
+ );
+ searchInput.value = "";
+ searchInput.doCommand();
+ await searchCompletedPromise;
+
+ // Checks if back to normal
+ is_element_visible(generalPane, "Should be in generalPane");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_searchsuggestions.js b/browser/components/preferences/tests/browser_searchsuggestions.js
new file mode 100644
index 0000000000..f3ce1615ea
--- /dev/null
+++ b/browser/components/preferences/tests/browser_searchsuggestions.js
@@ -0,0 +1,128 @@
+const SUGGEST_PREF_NAME = "browser.search.suggest.enabled";
+const URLBAR_SUGGEST_PREF_NAME = "browser.urlbar.suggest.searches";
+const PRIVATE_PREF_NAME = "browser.search.suggest.enabled.private";
+
+let initialUrlbarSuggestValue;
+let initialSuggestionsInPrivateValue;
+
+add_setup(async function () {
+ const originalSuggest = Services.prefs.getBoolPref(SUGGEST_PREF_NAME);
+ initialUrlbarSuggestValue = Services.prefs.getBoolPref(
+ URLBAR_SUGGEST_PREF_NAME
+ );
+ initialSuggestionsInPrivateValue =
+ Services.prefs.getBoolPref(PRIVATE_PREF_NAME);
+
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref(SUGGEST_PREF_NAME, originalSuggest);
+ Services.prefs.setBoolPref(
+ PRIVATE_PREF_NAME,
+ initialSuggestionsInPrivateValue
+ );
+ });
+});
+
+// Open with suggestions enabled
+add_task(async function test_suggestions_start_enabled() {
+ Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true);
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let urlbarBox = doc.getElementById("urlBarSuggestion");
+ let privateBox = doc.getElementById("showSearchSuggestionsPrivateWindows");
+ ok(!urlbarBox.disabled, "Should have enabled the urlbar checkbox");
+ ok(
+ !privateBox.disabled,
+ "Should have enabled the private mode suggestions checkbox"
+ );
+ is(
+ urlbarBox.checked,
+ initialUrlbarSuggestValue,
+ "Should have the correct value for the urlbar checkbox"
+ );
+ is(
+ privateBox.checked,
+ initialSuggestionsInPrivateValue,
+ "Should have the correct value for the private mode suggestions checkbox"
+ );
+
+ async function toggleElement(id, prefName, element, initialValue, desc) {
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ `#${id}`,
+ {},
+ gBrowser.selectedBrowser
+ );
+ is(
+ element.checked,
+ !initialValue,
+ `Should have flipped the ${desc} checkbox`
+ );
+ let prefValue = Services.prefs.getBoolPref(prefName);
+ is(
+ prefValue,
+ !initialValue,
+ `Should have updated the ${desc} preference value`
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ `#${id}`,
+ {},
+ gBrowser.selectedBrowser
+ );
+ is(
+ element.checked,
+ initialValue,
+ `Should have flipped the ${desc} checkbox back to the original value`
+ );
+ prefValue = Services.prefs.getBoolPref(prefName);
+ is(
+ prefValue,
+ initialValue,
+ `Should have updated the ${desc} preference back to the original value`
+ );
+ }
+
+ await toggleElement(
+ "urlBarSuggestion",
+ URLBAR_SUGGEST_PREF_NAME,
+ urlbarBox,
+ initialUrlbarSuggestValue,
+ "urlbar"
+ );
+ await toggleElement(
+ "showSearchSuggestionsPrivateWindows",
+ PRIVATE_PREF_NAME,
+ privateBox,
+ initialSuggestionsInPrivateValue,
+ "private suggestion"
+ );
+
+ Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false);
+ ok(!urlbarBox.checked, "Should have unchecked the urlbar box");
+ ok(urlbarBox.disabled, "Should have disabled the urlbar box");
+ ok(!privateBox.checked, "Should have unchecked the private suggestions box");
+ ok(privateBox.disabled, "Should have disabled the private suggestions box");
+
+ gBrowser.removeCurrentTab();
+});
+
+// Open with suggestions disabled
+add_task(async function test_suggestions_start_disabled() {
+ Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false);
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let urlbarBox = doc.getElementById("urlBarSuggestion");
+ ok(urlbarBox.disabled, "Should have the urlbar box disabled");
+ let privateBox = doc.getElementById("showSearchSuggestionsPrivateWindows");
+ ok(privateBox.disabled, "Should have the private suggestions box disabled");
+
+ Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true);
+
+ ok(!urlbarBox.disabled, "Should have enabled the urlbar box");
+ ok(!privateBox.disabled, "Should have enabled the private suggestions box");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/tests/browser_security-1.js b/browser/components/preferences/tests/browser_security-1.js
new file mode 100644
index 0000000000..80f3e6902b
--- /dev/null
+++ b/browser/components/preferences/tests/browser_security-1.js
@@ -0,0 +1,106 @@
+const PREFS = [
+ "browser.safebrowsing.phishing.enabled",
+ "browser.safebrowsing.malware.enabled",
+
+ "browser.safebrowsing.downloads.enabled",
+
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted",
+ "browser.safebrowsing.downloads.remote.block_uncommon",
+];
+
+let originals = PREFS.map(pref => [pref, Services.prefs.getBoolPref(pref)]);
+let originalMalwareTable = Services.prefs.getCharPref(
+ "urlclassifier.malwareTable"
+);
+registerCleanupFunction(function () {
+ originals.forEach(([pref, val]) => Services.prefs.setBoolPref(pref, val));
+ Services.prefs.setCharPref(
+ "urlclassifier.malwareTable",
+ originalMalwareTable
+ );
+});
+
+// This test only opens the Preferences once, and then reloads the page
+// each time that it wants to test various preference combinations. We
+// only use one tab (instead of opening/closing for each test) for all
+// to help improve test times on debug builds.
+add_setup(async function () {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+// test the safebrowsing preference
+add_task(async function () {
+ async function checkPrefSwitch(val1, val2) {
+ Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled", val1);
+ Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", val2);
+
+ gBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let checkbox = doc.getElementById("enableSafeBrowsing");
+ let blockDownloads = doc.getElementById("blockDownloads");
+ let blockUncommon = doc.getElementById("blockUncommonUnwanted");
+ let checked = checkbox.checked;
+ is(
+ blockDownloads.hasAttribute("disabled"),
+ !checked,
+ "block downloads checkbox is set correctly"
+ );
+
+ is(
+ checked,
+ val1 && val2,
+ "safebrowsing preference is initialized correctly"
+ );
+ // should be disabled when checked is false (= pref is turned off)
+ is(
+ blockUncommon.hasAttribute("disabled"),
+ !checked,
+ "block uncommon checkbox is set correctly"
+ );
+
+ // scroll the checkbox into the viewport and click checkbox
+ checkbox.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(
+ checkbox,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ // check that both settings are now turned on or off
+ is(
+ Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled"),
+ !checked,
+ "safebrowsing.enabled is set correctly"
+ );
+ is(
+ Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled"),
+ !checked,
+ "safebrowsing.malware.enabled is set correctly"
+ );
+
+ // check if the other checkboxes have updated
+ checked = checkbox.checked;
+ if (blockDownloads) {
+ is(
+ blockDownloads.hasAttribute("disabled"),
+ !checked,
+ "block downloads checkbox is set correctly"
+ );
+ is(
+ blockUncommon.hasAttribute("disabled"),
+ !checked || !blockDownloads.checked,
+ "block uncommon checkbox is set correctly"
+ );
+ }
+ }
+
+ await checkPrefSwitch(true, true);
+ await checkPrefSwitch(false, true);
+ await checkPrefSwitch(true, false);
+ await checkPrefSwitch(false, false);
+});
diff --git a/browser/components/preferences/tests/browser_security-2.js b/browser/components/preferences/tests/browser_security-2.js
new file mode 100644
index 0000000000..88be366810
--- /dev/null
+++ b/browser/components/preferences/tests/browser_security-2.js
@@ -0,0 +1,177 @@
+const PREFS = [
+ "browser.safebrowsing.phishing.enabled",
+ "browser.safebrowsing.malware.enabled",
+
+ "browser.safebrowsing.downloads.enabled",
+
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted",
+ "browser.safebrowsing.downloads.remote.block_uncommon",
+];
+
+let originals = PREFS.map(pref => [pref, Services.prefs.getBoolPref(pref)]);
+let originalMalwareTable = Services.prefs.getCharPref(
+ "urlclassifier.malwareTable"
+);
+registerCleanupFunction(function () {
+ originals.forEach(([pref, val]) => Services.prefs.setBoolPref(pref, val));
+ Services.prefs.setCharPref(
+ "urlclassifier.malwareTable",
+ originalMalwareTable
+ );
+});
+
+// This test only opens the Preferences once, and then reloads the page
+// each time that it wants to test various preference combinations. We
+// only use one tab (instead of opening/closing for each test) for all
+// to help improve test times on debug builds.
+add_setup(async function () {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+// test the download protection preference
+add_task(async function () {
+ async function checkPrefSwitch(val) {
+ Services.prefs.setBoolPref("browser.safebrowsing.downloads.enabled", val);
+
+ gBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let checkbox = doc.getElementById("blockDownloads");
+
+ let blockUncommon = doc.getElementById("blockUncommonUnwanted");
+ let checked = checkbox.checked;
+ is(checked, val, "downloads preference is initialized correctly");
+ // should be disabled when val is false (= pref is turned off)
+ is(
+ blockUncommon.hasAttribute("disabled"),
+ !val,
+ "block uncommon checkbox is set correctly"
+ );
+
+ // scroll the checkbox into view, otherwise the synthesizeMouseAtCenter will be ignored, and click it
+ checkbox.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(
+ checkbox,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ // check that setting is now turned on or off
+ is(
+ Services.prefs.getBoolPref("browser.safebrowsing.downloads.enabled"),
+ !checked,
+ "safebrowsing.downloads preference is set correctly"
+ );
+
+ // check if the uncommon warning checkbox has updated
+ is(
+ blockUncommon.hasAttribute("disabled"),
+ val,
+ "block uncommon checkbox is set correctly"
+ );
+ }
+
+ await checkPrefSwitch(true);
+ await checkPrefSwitch(false);
+});
+
+requestLongerTimeout(2);
+// test the unwanted/uncommon software warning preference
+add_task(async function () {
+ async function checkPrefSwitch(val1, val2, isV2) {
+ Services.prefs.setBoolPref(
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted",
+ val1
+ );
+ Services.prefs.setBoolPref(
+ "browser.safebrowsing.downloads.remote.block_uncommon",
+ val2
+ );
+ let testMalwareTable = "goog-malware-" + (isV2 ? "shavar" : "proto");
+ testMalwareTable += ",test-malware-simple";
+ if (val1 && val2) {
+ testMalwareTable += ",goog-unwanted-" + (isV2 ? "shavar" : "proto");
+ testMalwareTable += ",moztest-unwanted-simple";
+ }
+ Services.prefs.setCharPref("urlclassifier.malwareTable", testMalwareTable);
+
+ gBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let checkbox = doc.getElementById("blockUncommonUnwanted");
+ let checked = checkbox.checked;
+ is(
+ checked,
+ val1 && val2,
+ "unwanted/uncommon preference is initialized correctly"
+ );
+
+ // scroll the checkbox into view, otherwise the synthesizeMouseAtCenter will be ignored, and click it
+ checkbox.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(
+ checkbox,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ // check that both settings are now turned on or off
+ is(
+ Services.prefs.getBoolPref(
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+ ),
+ !checked,
+ "block_potentially_unwanted is set correctly"
+ );
+ is(
+ Services.prefs.getBoolPref(
+ "browser.safebrowsing.downloads.remote.block_uncommon"
+ ),
+ !checked,
+ "block_uncommon is set correctly"
+ );
+
+ // when the preference is on, the malware table should include these ids
+ let malwareTable = Services.prefs
+ .getCharPref("urlclassifier.malwareTable")
+ .split(",");
+ if (isV2) {
+ is(
+ malwareTable.includes("goog-unwanted-shavar"),
+ !checked,
+ "malware table doesn't include goog-unwanted-shavar"
+ );
+ } else {
+ is(
+ malwareTable.includes("goog-unwanted-proto"),
+ !checked,
+ "malware table doesn't include goog-unwanted-proto"
+ );
+ }
+ is(
+ malwareTable.includes("moztest-unwanted-simple"),
+ !checked,
+ "malware table doesn't include moztest-unwanted-simple"
+ );
+ let sortedMalware = malwareTable.slice(0);
+ sortedMalware.sort();
+ Assert.deepEqual(
+ malwareTable,
+ sortedMalware,
+ "malware table has been sorted"
+ );
+ }
+
+ await checkPrefSwitch(true, true, false);
+ await checkPrefSwitch(false, true, false);
+ await checkPrefSwitch(true, false, false);
+ await checkPrefSwitch(false, false, false);
+ await checkPrefSwitch(true, true, true);
+ await checkPrefSwitch(false, true, true);
+ await checkPrefSwitch(true, false, true);
+ await checkPrefSwitch(false, false, true);
+});
diff --git a/browser/components/preferences/tests/browser_security-3.js b/browser/components/preferences/tests/browser_security-3.js
new file mode 100644
index 0000000000..75872d6629
--- /dev/null
+++ b/browser/components/preferences/tests/browser_security-3.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ registerCleanupFunction(async function () {
+ Services.prefs.unlockPref("browser.safebrowsing.phishing.enabled");
+ Services.prefs.unlockPref("browser.safebrowsing.malware.enabled");
+ Services.prefs.unlockPref("browser.safebrowsing.downloads.enabled");
+ Services.prefs.unlockPref(
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+ );
+ Services.prefs.unlockPref(
+ "browser.safebrowsing.downloads.remote.block_uncommon"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+// This test just reloads the preferences page for the various tests.
+add_task(async function () {
+ Services.prefs.lockPref("browser.safebrowsing.phishing.enabled");
+ Services.prefs.lockPref("browser.safebrowsing.malware.enabled");
+ Services.prefs.lockPref("browser.safebrowsing.downloads.enabled");
+ Services.prefs.lockPref(
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+ );
+ Services.prefs.lockPref(
+ "browser.safebrowsing.downloads.remote.block_uncommon"
+ );
+
+ gBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ is(
+ doc.getElementById("enableSafeBrowsing").disabled,
+ true,
+ "Safe browsing should be disabled"
+ );
+ is(
+ doc.getElementById("blockDownloads").disabled,
+ true,
+ "Block downloads should be disabled"
+ );
+ is(
+ doc.getElementById("blockUncommonUnwanted").disabled,
+ true,
+ "Block common unwanted should be disabled"
+ );
+
+ Services.prefs.unlockPref("browser.safebrowsing.phishing.enabled");
+ Services.prefs.unlockPref("browser.safebrowsing.malware.enabled");
+
+ gBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ doc = gBrowser.selectedBrowser.contentDocument;
+
+ let checkbox = doc.getElementById("enableSafeBrowsing");
+ checkbox.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(
+ checkbox,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ is(
+ doc.getElementById("blockDownloads").disabled,
+ true,
+ "Block downloads should be disabled"
+ );
+ is(
+ doc.getElementById("blockUncommonUnwanted").disabled,
+ true,
+ "Block common unwanted should be disabled"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ checkbox,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ is(
+ doc.getElementById("blockDownloads").disabled,
+ true,
+ "Block downloads should be disabled"
+ );
+ is(
+ doc.getElementById("blockUncommonUnwanted").disabled,
+ true,
+ "Block common unwanted should be disabled"
+ );
+
+ Services.prefs.unlockPref("browser.safebrowsing.downloads.enabled");
+
+ gBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ doc = gBrowser.selectedBrowser.contentDocument;
+
+ checkbox = doc.getElementById("blockDownloads");
+ checkbox.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(
+ checkbox,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ is(
+ doc.getElementById("blockUncommonUnwanted").disabled,
+ true,
+ "Block common unwanted should be disabled"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ checkbox,
+ {},
+ gBrowser.selectedBrowser.contentWindow
+ );
+
+ is(
+ doc.getElementById("blockUncommonUnwanted").disabled,
+ true,
+ "Block common unwanted should be disabled"
+ );
+});
diff --git a/browser/components/preferences/tests/browser_site_login_exceptions.js b/browser/components/preferences/tests/browser_site_login_exceptions.js
new file mode 100644
index 0000000000..508232b234
--- /dev/null
+++ b/browser/components/preferences/tests/browser_site_login_exceptions.js
@@ -0,0 +1,101 @@
+"use strict";
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml";
+
+var exceptionsDialog;
+
+add_task(async function openLoginExceptionsSubDialog() {
+ // ensure rememberSignons is off for this test;
+ ok(
+ !Services.prefs.getBoolPref("signon.rememberSignons"),
+ "Check initial value of signon.rememberSignons pref"
+ );
+
+ // Undo the save password change.
+ registerCleanupFunction(async function () {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let doc = content.document;
+ let savePasswordCheckBox = doc.getElementById("savePasswords");
+ if (savePasswordCheckBox.checked) {
+ savePasswordCheckBox.click();
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let doc = content.document;
+ let savePasswordCheckBox = doc.getElementById("savePasswords");
+ Assert.ok(
+ !savePasswordCheckBox.checked,
+ "Save Password CheckBox should be unchecked by default"
+ );
+ savePasswordCheckBox.click();
+
+ let loginExceptionsButton = doc.getElementById("passwordExceptions");
+ loginExceptionsButton.click();
+ });
+
+ exceptionsDialog = await dialogOpened;
+});
+
+add_task(async function addALoginException() {
+ let doc = exceptionsDialog.document;
+
+ let richlistbox = doc.getElementById("permissionsBox");
+ Assert.equal(richlistbox.itemCount, 0, "Row count should initially be 0");
+
+ let inputBox = doc.getElementById("url");
+ inputBox.focus();
+
+ EventUtils.sendString("www.example.com", exceptionsDialog);
+
+ let btnBlock = doc.getElementById("btnBlock");
+ btnBlock.click();
+
+ await TestUtils.waitForCondition(() => richlistbox.itemCount == 2);
+
+ let expectedResult = ["http://www.example.com", "https://www.example.com"];
+ for (let website of expectedResult) {
+ let elements = richlistbox.getElementsByAttribute("origin", website);
+ is(elements.length, 1, "It should find only one coincidence");
+ }
+});
+
+add_task(async function deleteALoginException() {
+ let doc = exceptionsDialog.document;
+
+ let richlistbox = doc.getElementById("permissionsBox");
+ let currentItems = 2;
+ Assert.equal(
+ richlistbox.itemCount,
+ currentItems,
+ `Row count should initially be ${currentItems}`
+ );
+ richlistbox.focus();
+
+ while (richlistbox.itemCount) {
+ richlistbox.selectedIndex = 0;
+
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_Backspace");
+ } else {
+ EventUtils.synthesizeKey("KEY_Delete");
+ }
+
+ currentItems -= 1;
+
+ await TestUtils.waitForCondition(
+ () => richlistbox.itemCount == currentItems
+ );
+ is_element_visible(
+ content.gSubDialog._dialogs[0]._box,
+ "Subdialog is visible after deleting an element"
+ );
+ }
+});
diff --git a/browser/components/preferences/tests/browser_site_login_exceptions_policy.js b/browser/components/preferences/tests/browser_site_login_exceptions_policy.js
new file mode 100644
index 0000000000..10f5039c6f
--- /dev/null
+++ b/browser/components/preferences/tests/browser_site_login_exceptions_policy.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+const PERMISSIONS_URL =
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml";
+
+var exceptionsDialog;
+
+add_task(async function openLoginExceptionsSubDialog() {
+ // ensure rememberSignons is off for this test;
+ ok(
+ !Services.prefs.getBoolPref("signon.rememberSignons"),
+ "Check initial value of signon.rememberSignons pref"
+ );
+
+ // Undo the save password change.
+ registerCleanupFunction(async function () {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let doc = content.document;
+ let savePasswordCheckBox = doc.getElementById("savePasswords");
+ if (savePasswordCheckBox.checked) {
+ savePasswordCheckBox.click();
+ }
+ });
+
+ gBrowser.removeCurrentTab();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ });
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ PasswordManagerExceptions: ["https://pwexception.example.com"],
+ },
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let doc = content.document;
+ let savePasswordCheckBox = doc.getElementById("savePasswords");
+ savePasswordCheckBox.click();
+
+ let loginExceptionsButton = doc.getElementById("passwordExceptions");
+ loginExceptionsButton.click();
+ });
+
+ exceptionsDialog = await dialogOpened;
+
+ let doc = exceptionsDialog.document;
+
+ let richlistbox = doc.getElementById("permissionsBox");
+ Assert.equal(richlistbox.itemCount, 1, `Row count should initially be 1`);
+
+ richlistbox.focus();
+ richlistbox.selectedIndex = 0;
+ Assert.ok(doc.getElementById("removePermission").disabled);
+});
diff --git a/browser/components/preferences/tests/browser_spotlight.js b/browser/components/preferences/tests/browser_spotlight.js
new file mode 100644
index 0000000000..4a1aae7ec1
--- /dev/null
+++ b/browser/components/preferences/tests/browser_spotlight.js
@@ -0,0 +1,72 @@
+add_task(async function test_openPreferences_spotlight() {
+ for (let [arg, expectedPane, expectedHash, expectedSubcategory] of [
+ ["privacy-reports", "panePrivacy", "#privacy", "reports"],
+ ["privacy-address-autofill", "panePrivacy", "#privacy", "address-autofill"],
+ [
+ "privacy-credit-card-autofill",
+ "panePrivacy",
+ "#privacy",
+ "credit-card-autofill",
+ ],
+ ["privacy-form-autofill", "panePrivacy", "#privacy", "form-autofill"],
+ ["privacy-logins", "panePrivacy", "#privacy", "logins"],
+ [
+ "privacy-trackingprotection",
+ "panePrivacy",
+ "#privacy",
+ "trackingprotection",
+ ],
+ [
+ "privacy-permissions-block-popups",
+ "panePrivacy",
+ "#privacy",
+ "permissions-block-popups",
+ ],
+ ]) {
+ if (
+ arg == "privacy-credit-card-autofill" &&
+ Services.prefs.getCharPref(
+ "extensions.formautofill.creditCards.supported"
+ ) == "off"
+ ) {
+ continue;
+ }
+ if (
+ arg == "privacy-address-autofill" &&
+ Services.prefs.getCharPref(
+ "extensions.formautofill.addresses.supported"
+ ) == "off"
+ ) {
+ continue;
+ }
+
+ let prefs = await openPreferencesViaOpenPreferencesAPI(arg, {
+ leaveOpen: true,
+ });
+ is(prefs.selectedPane, expectedPane, "The right pane is selected");
+ let doc = gBrowser.contentDocument;
+ is(
+ doc.location.hash,
+ expectedHash,
+ "The subcategory should be removed from the URI"
+ );
+ await TestUtils.waitForCondition(
+ () => doc.querySelector(".spotlight"),
+ "Wait for the spotlight"
+ );
+ is(
+ doc.querySelector(".spotlight").getAttribute("data-subcategory"),
+ expectedSubcategory,
+ "The right subcategory is spotlighted"
+ );
+
+ doc.defaultView.spotlight(null);
+ is(
+ doc.querySelector(".spotlight"),
+ null,
+ "The spotlighted section is cleared"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
diff --git a/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js b/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js
new file mode 100644
index 0000000000..73d12a1bf9
--- /dev/null
+++ b/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js
@@ -0,0 +1,124 @@
+"use strict";
+
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior";
+const COOKIE_BEHAVIOR_PBM_PREF = "network.cookie.cookieBehavior.pbmode";
+
+const CB_STRICT_FEATURES_PREF = "browser.contentblocking.features.strict";
+const FPI_PREF = "privacy.firstparty.isolate";
+
+async function testCookieBlockingInfoStandard(
+ cookieBehavior,
+ cookieBehaviorPBM,
+ isShown
+) {
+ let defaults = Services.prefs.getDefaultBranch("");
+ defaults.setIntPref(COOKIE_BEHAVIOR_PREF, cookieBehavior);
+ defaults.setIntPref(COOKIE_BEHAVIOR_PBM_PREF, cookieBehaviorPBM);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.contentDocument;
+
+ // Select standard mode.
+ let standardRadioOption = doc.getElementById("standardRadio");
+ standardRadioOption.click();
+
+ // Check the cookie blocking info for private windows for standard mode.
+ let elts = doc.querySelectorAll(
+ "#contentBlockingOptionStandard .extra-information-label.all-third-party-cookies-private-windows-option"
+ );
+ for (let elt of elts) {
+ is(
+ elt.hidden,
+ !isShown,
+ `The visibility of cookie blocking info for standard mode is correct`
+ );
+ }
+
+ gBrowser.removeCurrentTab();
+}
+
+async function testCookieBlockingInfoStrict(
+ contentBlockingStrictFeatures,
+ isShown
+) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [CB_STRICT_FEATURES_PREF, contentBlockingStrictFeatures],
+ [FPI_PREF, false],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.contentDocument;
+
+ // Select strict mode.
+ let strictRadioOption = doc.getElementById("strictRadio");
+ strictRadioOption.click();
+
+ // Check the cookie blocking info for private windows for strict mode.
+ let elts = doc.querySelectorAll(
+ "#contentBlockingOptionStrict .extra-information-label.all-third-party-cookies-private-windows-option"
+ );
+ for (let elt of elts) {
+ is(
+ elt.hidden,
+ !isShown,
+ `The cookie blocking info is hidden for strict mode`
+ );
+ }
+
+ gBrowser.removeCurrentTab();
+}
+
+add_task(async function runTests() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FPI_PREF, false]],
+ });
+
+ let defaults = Services.prefs.getDefaultBranch("");
+ let originalCookieBehavior = defaults.getIntPref(COOKIE_BEHAVIOR_PREF);
+ let originalCookieBehaviorPBM = defaults.getIntPref(COOKIE_BEHAVIOR_PBM_PREF);
+
+ // Test if the cookie blocking info for state partitioning in PBM is
+ // shown in standard mode if the regular cookieBehavior is
+ // BEHAVIOR_REJECT_TRACKER and the private cookieBehavior is
+ // BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ await testCookieBlockingInfoStandard(
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ true
+ );
+
+ // Test if the cookie blocking info is hidden in standard mode if both
+ // cookieBehaviors are the same.
+ await testCookieBlockingInfoStandard(
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ false
+ );
+
+ // Test if the cookie blocking info is hidden for strict mode if
+ // cookieBehaviors both are BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN in
+ // the strict feature value.
+ await testCookieBlockingInfoStrict(
+ "tp,tpPrivate,cookieBehavior5,cookieBehaviorPBM5,cm,fp,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp",
+ false
+ );
+
+ // Test if the cookie blocking info is shown for strict mode if the regular
+ // cookieBehavior is BEHAVIOR_REJECT_TRACKER and the private cookieBehavior is
+ // BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ await testCookieBlockingInfoStrict(
+ "tp,tpPrivate,cookieBehavior4,cookieBehaviorPBM5,cm,fp,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp",
+ true
+ );
+
+ defaults.setIntPref(COOKIE_BEHAVIOR_PREF, originalCookieBehavior);
+ defaults.setIntPref(COOKIE_BEHAVIOR_PBM_PREF, originalCookieBehaviorPBM);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/preferences/tests/browser_statePartitioning_strings.js b/browser/components/preferences/tests/browser_statePartitioning_strings.js
new file mode 100644
index 0000000000..aed7e26977
--- /dev/null
+++ b/browser/components/preferences/tests/browser_statePartitioning_strings.js
@@ -0,0 +1,79 @@
+"use strict";
+
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const CB_STRICT_FEATURES_PREF = "browser.contentblocking.features.strict";
+const CB_STRICT_FEATURES_VALUE = "tp,tpPrivate,cookieBehavior5,cm,fp,stp,lvl2";
+const FPI_PREF = "privacy.firstparty.isolate";
+const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior";
+const COOKIE_BEHAVIOR_VALUE = 5;
+
+async function testStrings() {
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ let doc = gBrowser.contentDocument;
+
+ // Check the cookie blocking info strings
+ let elts = doc.querySelectorAll(
+ ".extra-information-label.cross-site-cookies-option"
+ );
+ for (let elt of elts) {
+ ok(!elt.hidden, "The new cross-site cookies info label is visible");
+ }
+
+ // Check the learn more strings
+ elts = doc.querySelectorAll(
+ ".tail-with-learn-more.content-blocking-warning-description"
+ );
+ for (let elt of elts) {
+ let id = doc.l10n.getAttributes(elt).id;
+ is(
+ id,
+ "content-blocking-and-isolating-etp-warning-description-2",
+ "The correct warning description string is in use"
+ );
+ }
+
+ // Check the cookie blocking mode menu option string
+ let elt = doc.querySelector("#isolateCookiesSocialMedia");
+ let id = doc.l10n.getAttributes(elt).id;
+ is(
+ id,
+ "sitedata-option-block-cross-site-cookies",
+ "The correct string is in use for the cookie blocking option"
+ );
+
+ // Check the FPI warning is hidden with FPI off
+ let warningElt = doc.getElementById("fpiIncompatibilityWarning");
+ is(warningElt.hidden, true, "The FPI warning is hidden");
+
+ gBrowser.removeCurrentTab();
+
+ // Check the FPI warning is shown only if MVP UI is enabled when FPI is on
+ await SpecialPowers.pushPrefEnv({ set: [[FPI_PREF, true]] });
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ doc = gBrowser.contentDocument;
+ warningElt = doc.getElementById("fpiIncompatibilityWarning");
+ ok(!warningElt.hidden, `The FPI warning is visible`);
+ await SpecialPowers.popPrefEnv();
+
+ gBrowser.removeCurrentTab();
+}
+
+add_task(async function runTests() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [CB_STRICT_FEATURES_PREF, CB_STRICT_FEATURES_VALUE],
+ [FPI_PREF, false],
+ ],
+ });
+ let defaults = Services.prefs.getDefaultBranch("");
+ let originalCookieBehavior = defaults.getIntPref(COOKIE_BEHAVIOR_PREF);
+ defaults.setIntPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIOR_VALUE);
+ registerCleanupFunction(() => {
+ defaults.setIntPref(COOKIE_BEHAVIOR_PREF, originalCookieBehavior);
+ });
+
+ await testStrings();
+});
diff --git a/browser/components/preferences/tests/browser_subdialogs.js b/browser/components/preferences/tests/browser_subdialogs.js
new file mode 100644
index 0000000000..9342575886
--- /dev/null
+++ b/browser/components/preferences/tests/browser_subdialogs.js
@@ -0,0 +1,639 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the sub-dialog infrastructure, not for actual sub-dialog functionality.
+ */
+
+const gDialogURL = getRootDirectory(gTestPath) + "subdialog.xhtml";
+const gDialogURL2 = getRootDirectory(gTestPath) + "subdialog2.xhtml";
+
+function open_subdialog_and_test_generic_start_state(
+ browser,
+ domcontentloadedFn,
+ url = gDialogURL
+) {
+ let domcontentloadedFnStr = domcontentloadedFn
+ ? "(" + domcontentloadedFn.toString() + ")()"
+ : "";
+ return SpecialPowers.spawn(
+ browser,
+ [{ url, domcontentloadedFnStr }],
+ async function (args) {
+ let rv = { acceptCount: 0 };
+ let win = content.window;
+ content.gSubDialog.open(args.url, undefined, rv);
+ let subdialog = content.gSubDialog._topDialog;
+
+ info("waiting for subdialog DOMFrameContentLoaded");
+ let dialogOpenPromise;
+ await new Promise(resolve => {
+ win.addEventListener(
+ "DOMFrameContentLoaded",
+ function frameContentLoaded(ev) {
+ // We can get events for loads in other frames, so we have to filter
+ // those out.
+ if (ev.target != subdialog._frame) {
+ return;
+ }
+ win.removeEventListener(
+ "DOMFrameContentLoaded",
+ frameContentLoaded
+ );
+ dialogOpenPromise = ContentTaskUtils.waitForEvent(
+ subdialog._overlay,
+ "dialogopen"
+ );
+ resolve();
+ },
+ { capture: true }
+ );
+ });
+ let result;
+ if (args.domcontentloadedFnStr) {
+ // eslint-disable-next-line no-eval
+ result = eval(args.domcontentloadedFnStr);
+ }
+
+ info("waiting for subdialog load");
+ await dialogOpenPromise;
+ info("subdialog window is loaded");
+
+ let expectedStyleSheetURLs = subdialog._injectedStyleSheets.slice(0);
+ for (let styleSheet of subdialog._frame.contentDocument.styleSheets) {
+ let index = expectedStyleSheetURLs.indexOf(styleSheet.href);
+ if (index >= 0) {
+ expectedStyleSheetURLs.splice(index, 1);
+ }
+ }
+
+ Assert.ok(
+ !!subdialog._frame.contentWindow,
+ "The dialog should be non-null"
+ );
+ Assert.notEqual(
+ subdialog._frame.contentWindow.location.toString(),
+ "about:blank",
+ "Subdialog URL should not be about:blank"
+ );
+ Assert.equal(
+ win.getComputedStyle(subdialog._overlay).visibility,
+ "visible",
+ "Overlay should be visible"
+ );
+ Assert.equal(
+ expectedStyleSheetURLs.length,
+ 0,
+ "No stylesheets that were expected are missing"
+ );
+ return result;
+ }
+ );
+}
+
+async function close_subdialog_and_test_generic_end_state(
+ browser,
+ closingFn,
+ closingButton,
+ acceptCount,
+ options
+) {
+ let getDialogsCount = () => {
+ return SpecialPowers.spawn(
+ browser,
+ [],
+ () => content.window.gSubDialog._dialogs.length
+ );
+ };
+ let getStackChildrenCount = () => {
+ return SpecialPowers.spawn(
+ browser,
+ [],
+ () => content.window.gSubDialog._dialogStack.children.length
+ );
+ };
+ let dialogclosingPromise = SpecialPowers.spawn(
+ browser,
+ [{ closingButton, acceptCount }],
+ async function (expectations) {
+ let win = content.window;
+ let subdialog = win.gSubDialog._topDialog;
+ let frame = subdialog._frame;
+
+ let frameWinUnload = ContentTaskUtils.waitForEvent(
+ frame.contentWindow,
+ "unload",
+ true
+ );
+
+ let actualAcceptCount;
+ info("waiting for dialogclosing");
+ info("URI " + frame.currentURI?.spec);
+ let closingEvent = await ContentTaskUtils.waitForEvent(
+ frame.contentWindow,
+ "dialogclosing",
+ true,
+ () => {
+ actualAcceptCount = frame.contentWindow?.arguments[0].acceptCount;
+ return true;
+ }
+ );
+
+ info("Waiting for subdialog unload");
+ await frameWinUnload;
+
+ let contentClosingButton = closingEvent.detail.button;
+
+ Assert.notEqual(
+ win.getComputedStyle(subdialog._overlay).visibility,
+ "visible",
+ "overlay is not visible"
+ );
+ Assert.equal(
+ frame.getAttribute("style"),
+ "",
+ "inline styles should be cleared"
+ );
+ Assert.equal(
+ contentClosingButton,
+ expectations.closingButton,
+ "closing event should indicate button was '" +
+ expectations.closingButton +
+ "'"
+ );
+ Assert.equal(
+ actualAcceptCount,
+ expectations.acceptCount,
+ "should be 1 if accepted, 0 if canceled, undefined if closed w/out button"
+ );
+ }
+ );
+ let initialDialogsCount = await getDialogsCount();
+ let initialStackChildrenCount = await getStackChildrenCount();
+ if (options && options.runClosingFnOutsideOfContentTask) {
+ await closingFn();
+ } else {
+ SpecialPowers.spawn(browser, [], closingFn);
+ }
+
+ await dialogclosingPromise;
+ let endDialogsCount = await getDialogsCount();
+ let endStackChildrenCount = await getStackChildrenCount();
+ Assert.equal(
+ initialDialogsCount - 1,
+ endDialogsCount,
+ "dialog count should decrease by 1"
+ );
+ Assert.equal(
+ initialStackChildrenCount - 1,
+ endStackChildrenCount,
+ "stack children count should decrease by 1"
+ );
+}
+
+let tab;
+
+add_task(async function test_initialize() {
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+});
+
+add_task(
+ async function check_titlebar_focus_returnval_titlechanges_accepting() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+
+ let domtitlechangedPromise = BrowserTestUtils.waitForEvent(
+ tab.linkedBrowser,
+ "DOMTitleChanged"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let dialog = content.window.gSubDialog._topDialog;
+ let dialogWin = dialog._frame.contentWindow;
+ let dialogTitleElement = dialog._titleElement;
+ Assert.equal(
+ dialogTitleElement.textContent,
+ "Sample sub-dialog",
+ "Title should be correct initially"
+ );
+ Assert.equal(
+ dialogWin.document.activeElement.value,
+ "Default text",
+ "Textbox with correct text is focused"
+ );
+ dialogWin.document.title = "Updated title";
+ });
+
+ info("waiting for DOMTitleChanged event");
+ await domtitlechangedPromise;
+
+ SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let dialogTitleElement =
+ content.window.gSubDialog._topDialog._titleElement;
+ Assert.equal(
+ dialogTitleElement.textContent,
+ "Updated title",
+ "subdialog should have updated title"
+ );
+ });
+
+ // Accept the dialog
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentDocument
+ .getElementById("subDialog")
+ .acceptDialog();
+ },
+ "accept",
+ 1
+ );
+ }
+);
+
+add_task(async function check_canceling_dialog() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+
+ info("canceling the dialog");
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentDocument
+ .getElementById("subDialog")
+ .cancelDialog();
+ },
+ "cancel",
+ 0
+ );
+});
+
+add_task(async function check_reopening_dialog() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+ info("opening another dialog which will close the first");
+ await open_subdialog_and_test_generic_start_state(
+ tab.linkedBrowser,
+ "",
+ gDialogURL2
+ );
+
+ SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let win = content.window;
+ let dialogs = win.gSubDialog._dialogs;
+ let lowerDialog = dialogs[0];
+ let topDialog = dialogs[1];
+ Assert.equal(dialogs.length, 2, "There should be two visible dialogs");
+ Assert.equal(
+ win.getComputedStyle(topDialog._overlay).visibility,
+ "visible",
+ "The top dialog should be visible"
+ );
+ Assert.equal(
+ win.getComputedStyle(lowerDialog._overlay).visibility,
+ "visible",
+ "The lower dialog should be visible"
+ );
+ Assert.equal(
+ win.getComputedStyle(topDialog._overlay).backgroundColor,
+ "rgba(0, 0, 0, 0.5)",
+ "The top dialog should have a semi-transparent overlay"
+ );
+ Assert.equal(
+ win.getComputedStyle(lowerDialog._overlay).backgroundColor,
+ "rgba(0, 0, 0, 0)",
+ "The lower dialog should not have an overlay"
+ );
+ });
+
+ info("closing two dialogs");
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentDocument
+ .getElementById("subDialog")
+ .acceptDialog();
+ },
+ "accept",
+ 1
+ );
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentDocument
+ .getElementById("subDialog")
+ .acceptDialog();
+ },
+ "accept",
+ 1
+ );
+});
+
+add_task(async function check_opening_while_closing() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+ info("closing");
+ content.window.gSubDialog._topDialog.close();
+ info("reopening immediately after calling .close()");
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentDocument
+ .getElementById("subDialog")
+ .acceptDialog();
+ },
+ "accept",
+ 1
+ );
+});
+
+add_task(async function window_close_on_dialog() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+
+ info("canceling the dialog");
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentWindow.close();
+ },
+ null,
+ 0
+ );
+});
+
+add_task(async function click_close_button_on_dialog() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+
+ info("canceling the dialog");
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ return BrowserTestUtils.synthesizeMouseAtCenter(
+ ".dialogClose",
+ {},
+ tab.linkedBrowser
+ );
+ },
+ null,
+ 0,
+ { runClosingFnOutsideOfContentTask: true }
+ );
+});
+
+add_task(async function background_click_should_close_dialog() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+
+ // Clicking on an inactive part of dialog itself should not close the dialog.
+ // Click the dialog title bar here to make sure nothing happens.
+ info("clicking the dialog title bar");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ ".dialogTitle",
+ {},
+ tab.linkedBrowser
+ );
+
+ // Close the dialog by clicking on the overlay background. Simulate a click
+ // at point (2,2) instead of (0,0) so we are sure we're clicking on the
+ // overlay background instead of some boundary condition that a real user
+ // would never click.
+ info("clicking the overlay background");
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ return BrowserTestUtils.synthesizeMouseAtPoint(
+ 2,
+ 2,
+ {},
+ tab.linkedBrowser
+ );
+ },
+ null,
+ 0,
+ { runClosingFnOutsideOfContentTask: true }
+ );
+});
+
+add_task(async function escape_should_close_dialog() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+
+ info("canceling the dialog");
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ return BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, tab.linkedBrowser);
+ },
+ "cancel",
+ 0,
+ { runClosingFnOutsideOfContentTask: true }
+ );
+});
+
+add_task(async function correct_width_and_height_should_be_used_for_dialog() {
+ await open_subdialog_and_test_generic_start_state(tab.linkedBrowser);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ function fuzzyEqual(value, expectedValue, fuzz, msg) {
+ Assert.greaterOrEqual(expectedValue + fuzz, value, msg);
+ Assert.lessOrEqual(expectedValue - fuzz, value, msg);
+ }
+ let topDialog = content.gSubDialog._topDialog;
+ let frameStyle = content.getComputedStyle(topDialog._frame);
+ let dialogStyle = topDialog.frameContentWindow.getComputedStyle(
+ topDialog.frameContentWindow.document.documentElement
+ );
+ let fontSize = parseFloat(dialogStyle.fontSize);
+ let height = parseFloat(frameStyle.height);
+ let width = parseFloat(frameStyle.width);
+
+ fuzzyEqual(
+ width,
+ fontSize * 32,
+ 2,
+ "Width should be set on the frame from the dialog"
+ );
+ fuzzyEqual(
+ height,
+ fontSize * 5,
+ 2,
+ "Height should be set on the frame from the dialog"
+ );
+ });
+
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentWindow.close();
+ },
+ null,
+ 0
+ );
+});
+
+add_task(
+ async function wrapped_text_in_dialog_should_have_expected_scrollHeight() {
+ let oldHeight = await open_subdialog_and_test_generic_start_state(
+ tab.linkedBrowser,
+ function domcontentloadedFn() {
+ let frame = content.window.gSubDialog._topDialog._frame;
+ let doc = frame.contentDocument;
+ let scrollHeight = doc.documentElement.scrollHeight;
+ doc.documentElement.style.removeProperty("height");
+ doc.getElementById("desc").textContent = `
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
+ laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
+ architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas
+ sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
+ laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
+ architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas
+ sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
+ laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
+ architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas
+ sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
+ voluptatem sequi nesciunt.`;
+ return scrollHeight;
+ }
+ );
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [oldHeight],
+ async function (contentOldHeight) {
+ function fuzzyEqual(value, expectedValue, fuzz, msg) {
+ Assert.greaterOrEqual(expectedValue + fuzz, value, msg);
+ Assert.lessOrEqual(expectedValue - fuzz, value, msg);
+ }
+ let topDialog = content.gSubDialog._topDialog;
+ let frame = topDialog._frame;
+ let frameStyle = content.getComputedStyle(frame);
+ let docEl = frame.contentDocument.documentElement;
+ let dialogStyle = topDialog.frameContentWindow.getComputedStyle(docEl);
+ let fontSize = parseFloat(dialogStyle.fontSize);
+ let height = parseFloat(frameStyle.height);
+ let width = parseFloat(frameStyle.width);
+
+ fuzzyEqual(
+ width,
+ 32 * fontSize,
+ 2,
+ "Width should be set on the frame from the dialog"
+ );
+ Assert.ok(
+ docEl.scrollHeight > contentOldHeight,
+ "Content height increased (from " +
+ contentOldHeight +
+ " to " +
+ docEl.scrollHeight +
+ ")."
+ );
+ fuzzyEqual(
+ height,
+ docEl.scrollHeight,
+ 2,
+ "Height on the frame should be higher now. " +
+ "This test may fail on certain screen resoluition. " +
+ "See bug 1420576 and bug 1205717."
+ );
+ }
+ );
+
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentWindow.window.close();
+ },
+ null,
+ 0
+ );
+ }
+);
+
+add_task(async function dialog_too_tall_should_get_reduced_in_height() {
+ await open_subdialog_and_test_generic_start_state(
+ tab.linkedBrowser,
+ function domcontentloadedFn() {
+ let frame = content.window.gSubDialog._topDialog._frame;
+ frame.contentDocument.documentElement.style.height = "100000px";
+ }
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ function fuzzyEqual(value, expectedValue, fuzz, msg) {
+ Assert.greaterOrEqual(expectedValue + fuzz, value, msg);
+ Assert.lessOrEqual(expectedValue - fuzz, value, msg);
+ }
+ let topDialog = content.gSubDialog._topDialog;
+ let frame = topDialog._frame;
+ let frameStyle = content.getComputedStyle(frame);
+ let dialogStyle = topDialog.frameContentWindow.getComputedStyle(
+ frame.contentDocument.documentElement
+ );
+ let fontSize = parseFloat(dialogStyle.fontSize);
+ let height = parseFloat(frameStyle.height);
+ let width = parseFloat(frameStyle.width);
+ fuzzyEqual(
+ width,
+ 32 * fontSize,
+ 2,
+ "Width should be set on the frame from the dialog"
+ );
+ Assert.less(
+ height,
+ content.window.innerHeight,
+ "Height on the frame should be smaller than window's innerHeight"
+ );
+ });
+
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentWindow.window.close();
+ },
+ null,
+ 0
+ );
+});
+
+add_task(
+ async function scrollWidth_and_scrollHeight_from_subdialog_should_size_the_browser() {
+ await open_subdialog_and_test_generic_start_state(
+ tab.linkedBrowser,
+ function domcontentloadedFn() {
+ let frame = content.window.gSubDialog._topDialog._frame;
+ frame.contentDocument.documentElement.style.removeProperty("height");
+ frame.contentDocument.documentElement.style.removeProperty("width");
+ }
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let frame = content.window.gSubDialog._topDialog._frame;
+ Assert.ok(
+ frame.style.width.endsWith("px"),
+ "Width (" +
+ frame.style.width +
+ ") should be set to a px value of the scrollWidth from the dialog"
+ );
+ let cs = content.getComputedStyle(frame);
+ Assert.stringMatches(
+ cs.getPropertyValue("--subdialog-inner-height"),
+ /px$/,
+ "Height (" +
+ cs.getPropertyValue("--subdialog-inner-height") +
+ ") should be set to a px value of the scrollHeight from the dialog"
+ );
+ });
+
+ await close_subdialog_and_test_generic_end_state(
+ tab.linkedBrowser,
+ function () {
+ content.window.gSubDialog._topDialog._frame.contentWindow.window.close();
+ },
+ null,
+ 0
+ );
+ }
+);
+
+add_task(async function test_shutdown() {
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js b/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js
new file mode 100644
index 0000000000..b36d9ecea3
--- /dev/null
+++ b/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+
+// This obj will be used in both tests
+// First test makes sure accepting the preferences matches these values
+// Second test makes sure the cancel dialog STILL matches these values
+const syncPrefs = {
+ "services.sync.engine.addons": false,
+ "services.sync.engine.bookmarks": true,
+ "services.sync.engine.history": true,
+ "services.sync.engine.tabs": false,
+ "services.sync.engine.prefs": false,
+ "services.sync.engine.passwords": false,
+ "services.sync.engine.addresses": false,
+ "services.sync.engine.creditcards": false,
+};
+
+add_setup(async () => {
+ UIState._internal.notifyStateUpdated = () => {};
+ const origNotifyStateUpdated = UIState._internal.notifyStateUpdated;
+ const origGet = UIState.get;
+ UIState.get = () => {
+ return { status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" };
+ };
+
+ registerCleanupFunction(() => {
+ UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
+ UIState.get = origGet;
+ });
+});
+
+/**
+ * We don't actually enable sync here, but we just check that the preferences are correct
+ * when the callback gets hit (accepting/cancelling the dialog)
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1584132.
+ */
+
+add_task(async function testDialogAccept() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ // This will check if the callback was actually called during the test
+ let callbackCalled = false;
+
+ // Enabling all the sync UI is painful in tests, so we just open the dialog manually
+ let syncWindow = await openAndLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml",
+ null,
+ {},
+ () => {
+ for (const [prefKey, prefValue] of Object.entries(syncPrefs)) {
+ Assert.equal(
+ Services.prefs.getBoolPref(prefKey),
+ prefValue,
+ `${prefValue} is expected value`
+ );
+ }
+ callbackCalled = true;
+ }
+ );
+
+ Assert.ok(syncWindow, "Choose what to sync window opened");
+ let syncChooseDialog =
+ syncWindow.document.getElementById("syncChooseOptions");
+ let syncCheckboxes = syncChooseDialog.querySelectorAll(
+ "checkbox[preference]"
+ );
+
+ // Adjust the checkbox values to the expectedValues in the list
+ [...syncCheckboxes].forEach(checkbox => {
+ if (syncPrefs[checkbox.getAttribute("preference")] !== checkbox.checked) {
+ checkbox.click();
+ }
+ });
+
+ syncChooseDialog.acceptDialog();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Assert.ok(callbackCalled, "Accept callback was called");
+});
+
+add_task(async function testDialogCancel() {
+ const cancelSyncPrefs = {
+ "services.sync.engine.addons": true,
+ "services.sync.engine.bookmarks": false,
+ "services.sync.engine.history": true,
+ "services.sync.engine.tabs": true,
+ "services.sync.engine.prefs": false,
+ "services.sync.engine.passwords": true,
+ "services.sync.engine.addresses": true,
+ "services.sync.engine.creditcards": false,
+ };
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", true]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ // This will check if the callback was actually called during the test
+ let callbackCalled = false;
+
+ // Enabling all the sync UI is painful in tests, so we just open the dialog manually
+ let syncWindow = await openAndLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml",
+ null,
+ {},
+ () => {
+ // We want to test against our previously accepted values in the last test
+ for (const [prefKey, prefValue] of Object.entries(syncPrefs)) {
+ Assert.equal(
+ Services.prefs.getBoolPref(prefKey),
+ prefValue,
+ `${prefValue} is expected value`
+ );
+ }
+ callbackCalled = true;
+ }
+ );
+
+ ok(syncWindow, "Choose what to sync window opened");
+ let syncChooseDialog =
+ syncWindow.document.getElementById("syncChooseOptions");
+ let syncCheckboxes = syncChooseDialog.querySelectorAll(
+ "checkbox[preference]"
+ );
+
+ // This time we're adjusting to the cancel list
+ [...syncCheckboxes].forEach(checkbox => {
+ if (
+ cancelSyncPrefs[checkbox.getAttribute("preference")] !== checkbox.checked
+ ) {
+ checkbox.click();
+ }
+ });
+
+ syncChooseDialog.cancelDialog();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Assert.ok(callbackCalled, "Cancel callback was called");
+});
+
+/**
+ * Tests that this subdialog can be opened via
+ * about:preferences?action=choose-what-to-sync#sync
+ */
+add_task(async function testDialogLaunchFromURI() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", true]],
+ });
+
+ let dialogEventPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "dialogopen",
+ true
+ );
+ await BrowserTestUtils.withNewTab(
+ "about:preferences?action=choose-what-to-sync#sync",
+ async browser => {
+ let dialogEvent = await dialogEventPromise;
+ Assert.equal(
+ dialogEvent.detail.dialog._frame.contentWindow.location,
+ "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml"
+ );
+ }
+ );
+});
diff --git a/browser/components/preferences/tests/browser_sync_disabled.js b/browser/components/preferences/tests/browser_sync_disabled.js
new file mode 100644
index 0000000000..1f8518a1e4
--- /dev/null
+++ b/browser/components/preferences/tests/browser_sync_disabled.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that we don't show sync pane when it's disabled.
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1536752.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", false]],
+ });
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ ok(
+ gBrowser.contentDocument.getElementById("category-sync").hidden,
+ "sync category hidden"
+ );
+
+ // Check that we don't get any results in sync when searching:
+ await evaluateSearchResults("sync", "no-results-message");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/browser_sync_pairing.js b/browser/components/preferences/tests/browser_sync_pairing.js
new file mode 100644
index 0000000000..6491007a38
--- /dev/null
+++ b/browser/components/preferences/tests/browser_sync_pairing.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+const { FxAccountsPairingFlow } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsPairing.sys.mjs"
+);
+
+// Use sinon for mocking.
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+let flowCounter = 0;
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("identity.fxaccounts.pairing.enabled", true);
+ // Sync start-up might interfere with our tests, don't let UIState send UI updates.
+ const origNotifyStateUpdated = UIState._internal.notifyStateUpdated;
+ UIState._internal.notifyStateUpdated = () => {};
+
+ const origGet = UIState.get;
+ UIState.get = () => {
+ return { status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" };
+ };
+
+ const origStart = FxAccountsPairingFlow.start;
+ FxAccountsPairingFlow.start = ({ emitter: e }) => {
+ return `https://foo.bar/${flowCounter++}`;
+ };
+
+ registerCleanupFunction(() => {
+ UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
+ UIState.get = origGet;
+ FxAccountsPairingFlow.start = origStart;
+ });
+});
+
+add_task(async function testShowsQRCode() {
+ await runWithPairingDialog(async win => {
+ let doc = win.document;
+ let qrContainer = doc.getElementById("qrContainer");
+ let qrWrapper = doc.getElementById("qrWrapper");
+
+ await TestUtils.waitForCondition(
+ () => qrWrapper.getAttribute("pairing-status") == "ready"
+ );
+
+ // Verify that a QRcode is being shown.
+ Assert.ok(
+ qrContainer.style.backgroundImage.startsWith(
+ `url("data:image/gif;base64,R0lGODdhOgA6AIAAAAAAAP///ywAAAAAOgA6AAAC/4yPqcvtD6OctNqLs968+w+G4gKU5nkiJYO2JuW6KsDGKEw3a7AbPZ+r4Ry7nzFIQkKKN6Avlzowo78`
+ )
+ );
+
+ // Close the dialog.
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ gBrowser.contentDocument.querySelector(".dialogClose").click();
+
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+ });
+});
+
+add_task(async function testCantShowQrCode() {
+ const origStart = FxAccountsPairingFlow.start;
+ FxAccountsPairingFlow.start = async () => {
+ throw new Error("boom");
+ };
+ await runWithPairingDialog(async win => {
+ let doc = win.document;
+ let qrWrapper = doc.getElementById("qrWrapper");
+
+ await TestUtils.waitForCondition(
+ () => qrWrapper.getAttribute("pairing-status") == "error"
+ );
+
+ // Close the dialog.
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ gBrowser.contentDocument.querySelector(".dialogClose").click();
+
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+ });
+ FxAccountsPairingFlow.start = origStart;
+});
+
+add_task(async function testSwitchToWebContent() {
+ await runWithPairingDialog(async win => {
+ let doc = win.document;
+ let qrWrapper = doc.getElementById("qrWrapper");
+
+ await TestUtils.waitForCondition(
+ () => qrWrapper.getAttribute("pairing-status") == "ready"
+ );
+
+ const spySwitchURL = sinon.spy(win.gFxaPairDeviceDialog, "_switchToUrl");
+ const emitter = win.gFxaPairDeviceDialog._emitter;
+ emitter.emit("view:SwitchToWebContent", "about:robots");
+
+ Assert.equal(spySwitchURL.callCount, 1);
+ });
+});
+
+add_task(async function testError() {
+ await runWithPairingDialog(async win => {
+ let doc = win.document;
+ let qrWrapper = doc.getElementById("qrWrapper");
+
+ await TestUtils.waitForCondition(
+ () => qrWrapper.getAttribute("pairing-status") == "ready"
+ );
+
+ const emitter = win.gFxaPairDeviceDialog._emitter;
+ emitter.emit("view:Error");
+
+ await TestUtils.waitForCondition(
+ () => qrWrapper.getAttribute("pairing-status") == "error"
+ );
+
+ // Close the dialog.
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ gBrowser.contentDocument.querySelector(".dialogClose").click();
+
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+ });
+});
+
+async function runWithPairingDialog(test) {
+ await openPreferencesViaOpenPreferencesAPI("paneSync", { leaveOpen: true });
+
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/fxaPairDevice.xhtml"
+ );
+ gBrowser.contentWindow.gSyncPane.pairAnotherDevice();
+
+ let win = await promiseSubDialogLoaded;
+
+ await test(win);
+
+ sinon.restore();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
diff --git a/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js b/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js
new file mode 100644
index 0000000000..8d1fa3c80b
--- /dev/null
+++ b/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function checkForPrompt(prefVal) {
+ return async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.history.custom", true],
+ ["browser.privatebrowsing.autostart", !prefVal],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
+ leaveOpen: true,
+ });
+ let doc = gBrowser.contentDocument;
+ is(
+ doc.getElementById("historyMode").value,
+ "custom",
+ "Expect custom history mode"
+ );
+
+ // Stub out the prompt method as an easy way to check it was shown. We throw away
+ // the tab straight after so don't need to bother restoring it.
+ let promptFired = false;
+ doc.defaultView.confirmRestartPrompt = () => {
+ promptFired = true;
+ return doc.defaultView.CONFIRM_RESTART_PROMPT_RESTART_NOW;
+ };
+ // Tick the checkbox and pretend the user did it:
+ let checkbox = doc.getElementById("privateBrowsingAutoStart");
+ checkbox.checked = prefVal;
+ checkbox.doCommand();
+
+ // Now the prompt should have shown.
+ ok(
+ promptFired,
+ `Expect a prompt when turning permanent private browsing ${
+ prefVal ? "on" : "off"
+ }!`
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ };
+}
+
+/**
+ * Check we show the prompt if the permanent private browsing pref is false
+ * and we flip the checkbox to true.
+ */
+add_task(checkForPrompt(true));
+
+/**
+ * Check it works in the other direction:
+ */
+add_task(checkForPrompt(false));
diff --git a/browser/components/preferences/tests/empty_pdf_file.pdf b/browser/components/preferences/tests/empty_pdf_file.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/components/preferences/tests/empty_pdf_file.pdf
diff --git a/browser/components/preferences/tests/engine1/manifest.json b/browser/components/preferences/tests/engine1/manifest.json
new file mode 100644
index 0000000000..5fa44ea692
--- /dev/null
+++ b/browser/components/preferences/tests/engine1/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "engine1",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine1@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine1",
+ "search_url": "https://1.example.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/preferences/tests/engine2/manifest.json b/browser/components/preferences/tests/engine2/manifest.json
new file mode 100644
index 0000000000..7ab094198b
--- /dev/null
+++ b/browser/components/preferences/tests/engine2/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "engine2",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "engine2@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine2",
+ "search_url": "https://2.example.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/preferences/tests/head.js b/browser/components/preferences/tests/head.js
new file mode 100644
index 0000000000..3eb126e1ae
--- /dev/null
+++ b/browser/components/preferences/tests/head.js
@@ -0,0 +1,334 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const kDefaultWait = 2000;
+
+function is_element_visible(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(!BrowserTestUtils.is_hidden(aElement), aMsg);
+}
+
+function is_element_hidden(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(BrowserTestUtils.is_hidden(aElement), aMsg);
+}
+
+function open_preferences(aCallback) {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:preferences");
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener(
+ "Initialized",
+ function () {
+ aCallback(gBrowser.contentWindow);
+ },
+ { capture: true, once: true }
+ );
+}
+
+function openAndLoadSubDialog(
+ aURL,
+ aFeatures = null,
+ aParams = null,
+ aClosingCallback = null
+) {
+ let promise = promiseLoadSubDialog(aURL);
+ content.gSubDialog.open(
+ aURL,
+ { features: aFeatures, closingCallback: aClosingCallback },
+ aParams
+ );
+ return promise;
+}
+
+function promiseLoadSubDialog(aURL) {
+ return new Promise((resolve, reject) => {
+ content.gSubDialog._dialogStack.addEventListener(
+ "dialogopen",
+ function dialogopen(aEvent) {
+ if (
+ aEvent.detail.dialog._frame.contentWindow.location == "about:blank"
+ ) {
+ return;
+ }
+ content.gSubDialog._dialogStack.removeEventListener(
+ "dialogopen",
+ dialogopen
+ );
+
+ is(
+ aEvent.detail.dialog._frame.contentWindow.location.toString(),
+ aURL,
+ "Check the proper URL is loaded"
+ );
+
+ // Check visibility
+ is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible");
+
+ // Check that stylesheets were injected
+ let expectedStyleSheetURLs =
+ aEvent.detail.dialog._injectedStyleSheets.slice(0);
+ for (let styleSheet of aEvent.detail.dialog._frame.contentDocument
+ .styleSheets) {
+ let i = expectedStyleSheetURLs.indexOf(styleSheet.href);
+ if (i >= 0) {
+ info("found " + styleSheet.href);
+ expectedStyleSheetURLs.splice(i, 1);
+ }
+ }
+ is(
+ expectedStyleSheetURLs.length,
+ 0,
+ "All expectedStyleSheetURLs should have been found"
+ );
+
+ // Wait for the next event tick to make sure the remaining part of the
+ // testcase runs after the dialog gets ready for input.
+ executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow));
+ }
+ );
+ });
+}
+
+async function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) {
+ let finalPaneEvent = Services.prefs.getBoolPref("identity.fxaccounts.enabled")
+ ? "sync-pane-loaded"
+ : "privacy-pane-loaded";
+ let finalPrefPaneLoaded = TestUtils.topicObserved(finalPaneEvent, () => true);
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ openPreferences(aPane, aOptions);
+ let newTabBrowser = gBrowser.selectedBrowser;
+
+ if (!newTabBrowser.contentWindow) {
+ await BrowserTestUtils.waitForEvent(newTabBrowser, "Initialized", true);
+ await BrowserTestUtils.waitForEvent(newTabBrowser.contentWindow, "load");
+ await finalPrefPaneLoaded;
+ }
+
+ let win = gBrowser.contentWindow;
+ let selectedPane = win.history.state;
+ if (!aOptions || !aOptions.leaveOpen) {
+ gBrowser.removeCurrentTab();
+ }
+ return { selectedPane };
+}
+
+async function runSearchInput(input) {
+ let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+ searchInput.focus();
+ let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.contentWindow,
+ "PreferencesSearchCompleted",
+ evt => evt.detail == input
+ );
+ EventUtils.sendString(input);
+ await searchCompletedPromise;
+}
+
+async function evaluateSearchResults(
+ keyword,
+ searchResults,
+ includeExperiments = false
+) {
+ searchResults = Array.isArray(searchResults)
+ ? searchResults
+ : [searchResults];
+ searchResults.push("header-searchResults");
+
+ await runSearchInput(keyword);
+
+ let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane");
+ for (let i = 0; i < mainPrefTag.childElementCount; i++) {
+ let child = mainPrefTag.children[i];
+ if (!includeExperiments && child.id?.startsWith("pane-experimental")) {
+ continue;
+ }
+ if (searchResults.includes(child.id)) {
+ is_element_visible(child, `${child.id} should be in search results`);
+ } else if (child.id) {
+ is_element_hidden(child, `${child.id} should not be in search results`);
+ }
+ }
+}
+
+function waitForMutation(target, opts, cb) {
+ return new Promise(resolve => {
+ let observer = new MutationObserver(() => {
+ if (!cb || cb(target)) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(target, opts);
+ });
+}
+
+// Used to add sample experimental features for testing. To use, create
+// a DefinitionServer, then call addDefinition as needed.
+class DefinitionServer {
+ constructor(definitionOverrides = []) {
+ let { HttpServer } = ChromeUtils.import(
+ "resource://testing-common/httpd.js"
+ );
+
+ this.server = new HttpServer();
+ this.server.registerPathHandler("/definitions.json", this);
+ this.definitions = {};
+
+ for (const override of definitionOverrides) {
+ this.addDefinition(override);
+ }
+
+ this.server.start();
+ registerCleanupFunction(
+ () => new Promise(resolve => this.server.stop(resolve))
+ );
+ }
+
+ // for nsIHttpRequestHandler
+ handle(request, response) {
+ response.write(JSON.stringify(this.definitions));
+ }
+
+ get definitionsUrl() {
+ const { primaryScheme, primaryHost, primaryPort } = this.server.identity;
+ return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`;
+ }
+
+ addDefinition(overrides = {}) {
+ const definition = {
+ id: "test-feature",
+ // These l10n IDs are just random so we have some text to display
+ title: "experimental-features-media-jxl",
+ description: "pane-experimental-description2",
+ restartRequired: false,
+ type: "boolean",
+ preference: "test.feature",
+ defaultValue: false,
+ isPublic: false,
+ ...overrides,
+ };
+ // convert targeted values, used by fromId
+ definition.isPublic = { default: definition.isPublic };
+ definition.defaultValue = { default: definition.defaultValue };
+ this.definitions[definition.id] = definition;
+ return definition;
+ }
+}
+
+/**
+ * Creates observer that waits for and then compares all perm-changes with the observances in order.
+ * @param {Array} observances permission changes to observe (order is important)
+ * @returns {Promise} Promise object that resolves once all permission changes have been observed
+ */
+function createObserveAllPromise(observances) {
+ // Create new promise that resolves once all items
+ // in observances array have been observed.
+ return new Promise(resolve => {
+ let permObserver = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != "perm-changed") {
+ return;
+ }
+
+ if (!observances.length) {
+ // See bug 1063410
+ return;
+ }
+
+ let permission = aSubject.QueryInterface(Ci.nsIPermission);
+ let expected = observances.shift();
+
+ info(
+ `observed perm-changed for ${permission.principal.origin} (remaining ${observances.length})`
+ );
+
+ is(aData, expected.data, "type of message should be the same");
+ for (let prop of ["type", "capability", "expireType"]) {
+ if (expected[prop]) {
+ is(
+ permission[prop],
+ expected[prop],
+ `property: "${prop}" should be equal (${permission.principal.origin})`
+ );
+ }
+ }
+
+ if (expected.origin) {
+ is(
+ permission.principal.origin,
+ expected.origin,
+ `property: "origin" should be equal (${permission.principal.origin})`
+ );
+ }
+
+ if (!observances.length) {
+ Services.obs.removeObserver(permObserver, "perm-changed");
+ executeSoon(resolve);
+ }
+ },
+ };
+ Services.obs.addObserver(permObserver, "perm-changed");
+ });
+}
+
+/**
+ * Waits for preference to be set and asserts the value.
+ * @param {string} pref - Preference key.
+ * @param {*} expectedValue - Expected value of the preference.
+ * @param {string} message - Assertion message.
+ */
+async function waitForAndAssertPrefState(pref, expectedValue, message) {
+ await TestUtils.waitForPrefChange(pref, value => {
+ if (value != expectedValue) {
+ return false;
+ }
+ is(value, expectedValue, message);
+ return true;
+ });
+}
+
+/**
+ * The Relay promo is not shown for distributions with a custom FxA instance,
+ * since Relay requires an account on our own server. These prefs are set to a
+ * dummy address by the test harness, filling the prefs with a "user value."
+ * This temporarily sets the default value equal to the dummy value, so that
+ * Firefox thinks we've configured the correct FxA server.
+ * @returns {Promise<MockFxAUtilityFunctions>} { mock, unmock }
+ */
+async function mockDefaultFxAInstance() {
+ /**
+ * @typedef {Object} MockFxAUtilityFunctions
+ * @property {function():void} mock - Makes the dummy values default, creating
+ * the illusion of a production FxA instance.
+ * @property {function():void} unmock - Restores the true defaults, creating
+ * the illusion of a custom FxA instance.
+ */
+
+ const defaultPrefs = Services.prefs.getDefaultBranch("");
+ const userPrefs = Services.prefs.getBranch("");
+ const realAuth = defaultPrefs.getCharPref("identity.fxaccounts.auth.uri");
+ const realRoot = defaultPrefs.getCharPref("identity.fxaccounts.remote.root");
+ const mockAuth = userPrefs.getCharPref("identity.fxaccounts.auth.uri");
+ const mockRoot = userPrefs.getCharPref("identity.fxaccounts.remote.root");
+ const mock = () => {
+ defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth);
+ defaultPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot);
+ userPrefs.clearUserPref("identity.fxaccounts.auth.uri");
+ userPrefs.clearUserPref("identity.fxaccounts.remote.root");
+ };
+ const unmock = () => {
+ defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", realAuth);
+ defaultPrefs.setCharPref("identity.fxaccounts.remote.root", realRoot);
+ userPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth);
+ userPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot);
+ };
+
+ mock();
+ registerCleanupFunction(unmock);
+
+ return { mock, unmock };
+}
diff --git a/browser/components/preferences/tests/privacypane_tests_perwindow.js b/browser/components/preferences/tests/privacypane_tests_perwindow.js
new file mode 100644
index 0000000000..8c376d1705
--- /dev/null
+++ b/browser/components/preferences/tests/privacypane_tests_perwindow.js
@@ -0,0 +1,388 @@
+// This file gets imported into the same scope as head.js.
+/* import-globals-from head.js */
+
+async function runTestOnPrivacyPrefPane(testFunc) {
+ info("runTestOnPrivacyPrefPane entered");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences",
+ true,
+ true
+ );
+ let browser = tab.linkedBrowser;
+ info("loaded about:preferences");
+ await browser.contentWindow.gotoPref("panePrivacy");
+ info("viewing privacy pane, executing testFunc");
+ await testFunc(browser.contentWindow);
+ BrowserTestUtils.removeTab(tab);
+}
+
+function controlChanged(element) {
+ element.doCommand();
+}
+
+// We can only test the panes that don't trigger a preference update
+function test_pane_visibility(win) {
+ let modes = {
+ remember: "historyRememberPane",
+ custom: "historyCustomPane",
+ };
+
+ let historymode = win.document.getElementById("historyMode");
+ ok(historymode, "history mode menulist should exist");
+ let historypane = win.document.getElementById("historyPane");
+ ok(historypane, "history mode pane should exist");
+
+ for (let mode in modes) {
+ historymode.value = mode;
+ controlChanged(historymode);
+ is(
+ historypane.selectedPanel,
+ win.document.getElementById(modes[mode]),
+ "The correct pane should be selected for the " + mode + " mode"
+ );
+ is_element_visible(
+ historypane.selectedPanel,
+ "Correct pane should be visible for the " + mode + " mode"
+ );
+ }
+}
+
+function test_dependent_elements(win) {
+ let historymode = win.document.getElementById("historyMode");
+ ok(historymode, "history mode menulist should exist");
+ let pbautostart = win.document.getElementById("privateBrowsingAutoStart");
+ ok(pbautostart, "the private browsing auto-start checkbox should exist");
+ let controls = [
+ win.document.getElementById("rememberHistory"),
+ win.document.getElementById("rememberForms"),
+ win.document.getElementById("deleteOnClose"),
+ win.document.getElementById("alwaysClear"),
+ ];
+ controls.forEach(function (control) {
+ ok(control, "the dependent controls should exist");
+ });
+ let independents = [
+ win.document.getElementById("contentBlockingBlockCookiesCheckbox"),
+ ];
+ independents.forEach(function (control) {
+ ok(control, "the independent controls should exist");
+ });
+ let cookieexceptions = win.document.getElementById("cookieExceptions");
+ ok(cookieexceptions, "the cookie exceptions button should exist");
+ let deleteOnCloseCheckbox = win.document.getElementById("deleteOnClose");
+ ok(deleteOnCloseCheckbox, "the delete on close checkbox should exist");
+ let alwaysclear = win.document.getElementById("alwaysClear");
+ ok(alwaysclear, "the clear data on close checkbox should exist");
+ let rememberhistory = win.document.getElementById("rememberHistory");
+ ok(rememberhistory, "the remember history checkbox should exist");
+ let rememberforms = win.document.getElementById("rememberForms");
+ ok(rememberforms, "the remember forms checkbox should exist");
+ let alwaysclearsettings = win.document.getElementById("clearDataSettings");
+ ok(alwaysclearsettings, "the clear data settings button should exist");
+
+ function expect_disabled(disabled) {
+ controls.forEach(function (control) {
+ is(
+ control.disabled,
+ disabled,
+ control.getAttribute("id") +
+ " should " +
+ (disabled ? "" : "not ") +
+ "be disabled"
+ );
+ });
+ if (disabled) {
+ ok(
+ !alwaysclear.checked,
+ "the clear data on close checkbox value should be as expected"
+ );
+ ok(
+ !rememberhistory.checked,
+ "the remember history checkbox value should be as expected"
+ );
+ ok(
+ !rememberforms.checked,
+ "the remember forms checkbox value should be as expected"
+ );
+ }
+ }
+ function check_independents(expected) {
+ independents.forEach(function (control) {
+ is(
+ control.disabled,
+ expected,
+ control.getAttribute("id") +
+ " should " +
+ (expected ? "" : "not ") +
+ "be disabled"
+ );
+ });
+
+ ok(
+ !cookieexceptions.disabled,
+ "the cookie exceptions button should never be disabled"
+ );
+ ok(
+ alwaysclearsettings.disabled,
+ "the clear data settings button should always be disabled"
+ );
+ }
+
+ // controls should only change in custom mode
+ historymode.value = "remember";
+ controlChanged(historymode);
+ expect_disabled(false);
+ check_independents(false);
+
+ // setting the mode to custom shouldn't change anything
+ historymode.value = "custom";
+ controlChanged(historymode);
+ expect_disabled(false);
+ check_independents(false);
+}
+
+function test_dependent_cookie_elements(win) {
+ let deleteOnCloseCheckbox = win.document.getElementById("deleteOnClose");
+ let deleteOnCloseNote = win.document.getElementById("deleteOnCloseNote");
+ let blockCookiesMenu = win.document.getElementById("blockCookiesMenu");
+
+ let controls = [blockCookiesMenu, deleteOnCloseCheckbox];
+ controls.forEach(function (control) {
+ ok(control, "the dependent cookie controls should exist");
+ });
+ let blockCookiesCheckbox = win.document.getElementById(
+ "contentBlockingBlockCookiesCheckbox"
+ );
+ ok(blockCookiesCheckbox, "the block cookies checkbox should exist");
+
+ function expect_disabled(disabled, c = controls) {
+ c.forEach(function (control) {
+ is(
+ control.disabled,
+ disabled,
+ control.getAttribute("id") +
+ " should " +
+ (disabled ? "" : "not ") +
+ "be disabled"
+ );
+ });
+ }
+
+ blockCookiesCheckbox.checked = true;
+ controlChanged(blockCookiesCheckbox);
+ expect_disabled(false);
+
+ blockCookiesCheckbox.checked = false;
+ controlChanged(blockCookiesCheckbox);
+ expect_disabled(true, [blockCookiesMenu]);
+ expect_disabled(false, [deleteOnCloseCheckbox]);
+ is_element_hidden(
+ deleteOnCloseNote,
+ "The notice for delete on close in permanent private browsing mode should be hidden."
+ );
+
+ blockCookiesMenu.value = "always";
+ controlChanged(blockCookiesMenu);
+ expect_disabled(true, [deleteOnCloseCheckbox]);
+ expect_disabled(false, [blockCookiesMenu]);
+ is_element_hidden(
+ deleteOnCloseNote,
+ "The notice for delete on close in permanent private browsing mode should be hidden."
+ );
+
+ if (win.contentBlockingCookiesAndSiteDataRejectTrackersEnabled) {
+ blockCookiesMenu.value = "trackers";
+ } else {
+ blockCookiesMenu.value = "unvisited";
+ }
+ controlChanged(blockCookiesMenu);
+ expect_disabled(false);
+
+ let historymode = win.document.getElementById("historyMode");
+
+ // The History mode setting for "never remember history" should still
+ // disable the "keep cookies until..." menu.
+ historymode.value = "dontremember";
+ controlChanged(historymode);
+ expect_disabled(true, [deleteOnCloseCheckbox]);
+ is_element_visible(
+ deleteOnCloseNote,
+ "The notice for delete on close in permanent private browsing mode should be visible."
+ );
+ expect_disabled(false, [blockCookiesMenu]);
+
+ historymode.value = "remember";
+ controlChanged(historymode);
+ expect_disabled(false);
+ is_element_hidden(
+ deleteOnCloseNote,
+ "The notice for delete on close in permanent private browsing mode should be hidden."
+ );
+}
+
+function test_dependent_clearonclose_elements(win) {
+ let historymode = win.document.getElementById("historyMode");
+ ok(historymode, "history mode menulist should exist");
+ let pbautostart = win.document.getElementById("privateBrowsingAutoStart");
+ ok(pbautostart, "the private browsing auto-start checkbox should exist");
+ let alwaysclear = win.document.getElementById("alwaysClear");
+ ok(alwaysclear, "the clear data on close checkbox should exist");
+ let alwaysclearsettings = win.document.getElementById("clearDataSettings");
+ ok(alwaysclearsettings, "the clear data settings button should exist");
+
+ function expect_disabled(disabled) {
+ is(
+ alwaysclearsettings.disabled,
+ disabled,
+ "the clear data settings should " +
+ (disabled ? "" : "not ") +
+ "be disabled"
+ );
+ }
+
+ historymode.value = "custom";
+ controlChanged(historymode);
+ pbautostart.checked = false;
+ controlChanged(pbautostart);
+ alwaysclear.checked = false;
+ controlChanged(alwaysclear);
+ expect_disabled(true);
+
+ alwaysclear.checked = true;
+ controlChanged(alwaysclear);
+ expect_disabled(false);
+
+ alwaysclear.checked = false;
+ controlChanged(alwaysclear);
+ expect_disabled(true);
+}
+
+async function test_dependent_prefs(win) {
+ let historymode = win.document.getElementById("historyMode");
+ ok(historymode, "history mode menulist should exist");
+ let controls = [
+ win.document.getElementById("rememberHistory"),
+ win.document.getElementById("rememberForms"),
+ ];
+ controls.forEach(function (control) {
+ ok(control, "the micro-management controls should exist");
+ });
+
+ function expect_checked(checked) {
+ controls.forEach(function (control) {
+ is(
+ control.checked,
+ checked,
+ control.getAttribute("id") +
+ " should " +
+ (checked ? "" : "not ") +
+ "be checked"
+ );
+ });
+ }
+
+ // controls should be checked in remember mode
+ historymode.value = "remember";
+ controlChanged(historymode);
+ // Initial updates from prefs are not sync, so wait:
+ await TestUtils.waitForCondition(
+ () => controls[0].getAttribute("checked") == "true"
+ );
+ expect_checked(true);
+
+ // even if they're unchecked in custom mode
+ historymode.value = "custom";
+ controlChanged(historymode);
+ controls.forEach(function (control) {
+ control.checked = false;
+ controlChanged(control);
+ });
+ expect_checked(false);
+ historymode.value = "remember";
+ controlChanged(historymode);
+ expect_checked(true);
+}
+
+function test_historymode_retention(mode, expect) {
+ return function test_historymode_retention_fn(win) {
+ let historymode = win.document.getElementById("historyMode");
+ ok(historymode, "history mode menulist should exist");
+
+ if (
+ (historymode.value == "remember" && mode == "dontremember") ||
+ (historymode.value == "dontremember" && mode == "remember") ||
+ (historymode.value == "custom" && mode == "dontremember")
+ ) {
+ return;
+ }
+
+ if (expect !== undefined) {
+ is(
+ historymode.value,
+ expect,
+ "history mode is expected to remain " + expect
+ );
+ }
+
+ historymode.value = mode;
+ controlChanged(historymode);
+ };
+}
+
+function test_custom_retention(controlToChange, expect, valueIncrement) {
+ return function test_custom_retention_fn(win) {
+ let historymode = win.document.getElementById("historyMode");
+ ok(historymode, "history mode menulist should exist");
+
+ if (expect !== undefined) {
+ is(
+ historymode.value,
+ expect,
+ "history mode is expected to remain " + expect
+ );
+ }
+
+ historymode.value = "custom";
+ controlChanged(historymode);
+
+ controlToChange = win.document.getElementById(controlToChange);
+ ok(controlToChange, "the control to change should exist");
+ switch (controlToChange.localName) {
+ case "checkbox":
+ controlToChange.checked = !controlToChange.checked;
+ break;
+ case "menulist":
+ controlToChange.value = valueIncrement;
+ break;
+ }
+ controlChanged(controlToChange);
+ };
+}
+
+const gPrefCache = new Map();
+
+function cache_preferences(win) {
+ let prefs = win.Preferences.getAll();
+ for (let pref of prefs) {
+ gPrefCache.set(pref.id, pref.value);
+ }
+}
+
+function reset_preferences(win) {
+ let prefs = win.Preferences.getAll();
+ // Avoid assigning undefined, which means clearing a "user"/test pref value
+ for (let pref of prefs) {
+ if (gPrefCache.has(pref.id)) {
+ pref.value = gPrefCache.get(pref.id);
+ }
+ }
+}
+
+function run_test_subset(subset) {
+ info("subset: " + Array.from(subset, x => x.name).join(",") + "\n");
+ let tests = [cache_preferences, ...subset, reset_preferences];
+ for (let test of tests) {
+ add_task(runTestOnPrivacyPrefPane.bind(undefined, test));
+ }
+}
diff --git a/browser/components/preferences/tests/siteData/browser.ini b/browser/components/preferences/tests/siteData/browser.ini
new file mode 100644
index 0000000000..deb8e82f7d
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+support-files =
+ head.js
+ site_data_test.html
+ service_worker_test.html
+ service_worker_test.js
+ offline/offline.html
+ offline/manifest.appcache
+
+[browser_clearSiteData.js]
+[browser_siteData.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_siteData2.js]
+skip-if =
+ win10_2004 && (!debug && !asan) # Bug 1669937
+ win11_2009 && (!debug && !asan) # Bug 1797751
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+ apple_catalina && debug # Bug 1775910
+[browser_siteData3.js]
+[browser_siteData_multi_select.js]
+skip-if = tsan # Bug 1683730
diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/tests/siteData/browser_clearSiteData.js
new file mode 100644
index 0000000000..217656707e
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/browser_clearSiteData.js
@@ -0,0 +1,219 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+async function testClearData(clearSiteData, clearCache) {
+ PermissionTestUtils.add(
+ TEST_QUOTA_USAGE_ORIGIN,
+ "persistent-storage",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Open a test site which saves into appcache.
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Fill indexedDB with test data.
+ // Don't wait for the page to load, to register the content event handler as quickly as possible.
+ // If this test goes intermittent, we might have to tell the page to wait longer before
+ // firing the event.
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false);
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "test-indexedDB-done",
+ false,
+ null,
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Register some service workers.
+ await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+ await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ // Test the initial states.
+ let cacheUsage = await SiteDataManager.getCacheSize();
+ let quotaUsage = await SiteDataTestUtils.getQuotaUsage(
+ TEST_QUOTA_USAGE_ORIGIN
+ );
+ let totalUsage = await SiteDataManager.getTotalUsage();
+ Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+ Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+ Assert.greater(totalUsage, 0, "The total usage should not be 0");
+
+ let initialSizeLabelValue = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let sizeLabel = content.document.getElementById("totalSiteDataSize");
+ return sizeLabel.textContent;
+ }
+ );
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
+
+ let dialogOpened = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml"
+ );
+ clearSiteDataButton.doCommand();
+ let dialogWin = await dialogOpened;
+
+ // Convert the usage numbers in the same way the UI does it to assert
+ // that they're displayed in the dialog.
+ let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage);
+ // For cache we just assert that the right unit (KB, probably) is displayed,
+ // since we've had cache intermittently changing under our feet.
+ let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage);
+
+ let clearSiteDataCheckbox =
+ dialogWin.document.getElementById("clearSiteData");
+ let clearCacheCheckbox = dialogWin.document.getElementById("clearCache");
+ // The usage details are filled asynchronously, so we assert that they're present by
+ // waiting for them to be filled in.
+ await Promise.all([
+ TestUtils.waitForCondition(
+ () =>
+ clearSiteDataCheckbox.label &&
+ clearSiteDataCheckbox.label.includes(convertedTotalUsage),
+ "Should show the quota usage"
+ ),
+ TestUtils.waitForCondition(
+ () =>
+ clearCacheCheckbox.label &&
+ clearCacheCheckbox.label.includes(convertedCacheUnit),
+ "Should show the cache usage"
+ ),
+ ]);
+
+ // Check the boxes according to our test input.
+ clearSiteDataCheckbox.checked = clearSiteData;
+ clearCacheCheckbox.checked = clearCache;
+
+ // Some additional promises/assertions to wait for
+ // when deleting site data.
+ let acceptPromise;
+ let updatePromise;
+ let cookiesClearedPromise;
+ if (clearSiteData) {
+ acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+ updatePromise = promiseSiteDataManagerSitesUpdated();
+ cookiesClearedPromise = promiseCookiesCleared();
+ }
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+
+ let clearButton = dialogWin.document
+ .querySelector("dialog")
+ .getButton("accept");
+ if (!clearSiteData && !clearCache) {
+ // Simulate user input on one of the checkboxes to trigger the event listener for
+ // disabling the clearButton.
+ clearCacheCheckbox.doCommand();
+ // Check that the clearButton gets disabled by unchecking both options.
+ await TestUtils.waitForCondition(
+ () => clearButton.disabled,
+ "Clear button should be disabled"
+ );
+ let cancelButton = dialogWin.document
+ .querySelector("dialog")
+ .getButton("cancel");
+ // Cancel, since we can't delete anything.
+ cancelButton.click();
+ } else {
+ // Delete stuff!
+ clearButton.click();
+ }
+
+ // For site data we display an extra warning dialog, make sure
+ // to accept it.
+ if (clearSiteData) {
+ await acceptPromise;
+ }
+
+ await dialogClosed;
+
+ if (clearCache) {
+ TestUtils.waitForCondition(async function () {
+ let usage = await SiteDataManager.getCacheSize();
+ return usage == 0;
+ }, "The cache usage should be removed");
+ } else {
+ Assert.greater(
+ await SiteDataManager.getCacheSize(),
+ 0,
+ "The cache usage should not be 0"
+ );
+ }
+
+ if (clearSiteData) {
+ await updatePromise;
+ await cookiesClearedPromise;
+ await promiseServiceWorkersCleared();
+
+ TestUtils.waitForCondition(async function () {
+ let usage = await SiteDataManager.getTotalUsage();
+ return usage == 0;
+ }, "The total usage should be removed");
+ } else {
+ quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+ totalUsage = await SiteDataManager.getTotalUsage();
+ Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+ Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ }
+
+ if (clearCache || clearSiteData) {
+ // Check that the size label in about:preferences updates after we cleared data.
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ initialSizeLabelValue }],
+ async function (opts) {
+ let sizeLabel = content.document.getElementById("totalSiteDataSize");
+ await ContentTaskUtils.waitForCondition(
+ () => sizeLabel.textContent != opts.initialSizeLabelValue,
+ "Site data size label should have updated."
+ );
+ }
+ );
+ }
+
+ let permission = PermissionTestUtils.getPermissionObject(
+ TEST_QUOTA_USAGE_ORIGIN,
+ "persistent-storage"
+ );
+ is(
+ clearSiteData ? permission : permission.capability,
+ clearSiteData ? null : Services.perms.ALLOW_ACTION,
+ "Should have the correct permission state."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SiteDataManager.removeAll();
+}
+
+// Test opening the "Clear All Data" dialog and cancelling.
+add_task(async function () {
+ await testClearData(false, false);
+});
+
+// Test opening the "Clear All Data" dialog and removing all site data.
+add_task(async function () {
+ await testClearData(true, false);
+});
+
+// Test opening the "Clear All Data" dialog and removing all cache.
+add_task(async function () {
+ await testClearData(false, true);
+});
+
+// Test opening the "Clear All Data" dialog and removing everything.
+add_task(async function () {
+ await testClearData(true, true);
+});
diff --git a/browser/components/preferences/tests/siteData/browser_siteData.js b/browser/components/preferences/tests/siteData/browser_siteData.js
new file mode 100644
index 0000000000..d3a73d6f4b
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/browser_siteData.js
@@ -0,0 +1,400 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function getPersistentStoragePermStatus(origin) {
+ let uri = Services.io.newURI(origin);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ return Services.perms.testExactPermissionFromPrincipal(
+ principal,
+ "persistent-storage"
+ );
+}
+
+// Test listing site using quota usage or site using appcache
+// This is currently disabled because of bug 1414751.
+add_task(async function () {
+ // Open a test site which would save into appcache
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Open a test site which would save into quota manager
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "test-indexedDB-done",
+ false,
+ null,
+ true
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ let updatedPromise = promiseSiteDataManagerSitesUpdated();
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatedPromise;
+ await openSiteDataSettingsDialog();
+ let dialog = content.gSubDialog._topDialog;
+ let dialogFrame = dialog._frame;
+ let frameDoc = dialogFrame.contentDocument;
+
+ let siteItems = frameDoc.getElementsByTagName("richlistitem");
+ is(siteItems.length, 2, "Should list sites using quota usage or appcache");
+
+ let appcacheSite = frameDoc.querySelector(
+ `richlistitem[host="${TEST_OFFLINE_HOST}"]`
+ );
+ ok(appcacheSite, "Should list site using appcache");
+
+ let qoutaUsageSite = frameDoc.querySelector(
+ `richlistitem[host="${TEST_QUOTA_USAGE_HOST}"]`
+ );
+ ok(qoutaUsageSite, "Should list site using quota usage");
+
+ // Always remember to clean up
+ await new Promise(resolve => {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ TEST_QUOTA_USAGE_ORIGIN
+ );
+ let request = Services.qms.clearStoragesForPrincipal(
+ principal,
+ null,
+ null,
+ true
+ );
+ request.callback = resolve;
+ });
+
+ await SiteDataManager.removeAll();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}).skip(); // Bug 1414751
+
+// Test buttons are disabled and loading message shown while updating sites
+add_task(async function () {
+ let updatedPromise = promiseSiteDataManagerSitesUpdated();
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatedPromise;
+ let cacheSize = await SiteDataManager.getCacheSize();
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let clearBtn = doc.getElementById("clearSiteDataButton");
+ let settingsButton = doc.getElementById("siteDataSettings");
+ let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize");
+ is(
+ clearBtn.disabled,
+ false,
+ "Should enable clear button after sites updated"
+ );
+ is(
+ settingsButton.disabled,
+ false,
+ "Should enable settings button after sites updated"
+ );
+ await SiteDataManager.getTotalUsage().then(usage => {
+ let [value, unit] = DownloadUtils.convertByteUnits(usage + cacheSize);
+ Assert.deepEqual(
+ doc.l10n.getAttributes(totalSiteDataSizeLabel),
+ {
+ id: "sitedata-total-size",
+ args: { value, unit },
+ },
+ "Should show the right total site data size"
+ );
+ });
+
+ Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
+ is(
+ clearBtn.disabled,
+ true,
+ "Should disable clear button while updating sites"
+ );
+ is(
+ settingsButton.disabled,
+ true,
+ "Should disable settings button while updating sites"
+ );
+ Assert.deepEqual(
+ doc.l10n.getAttributes(totalSiteDataSizeLabel),
+ {
+ id: "sitedata-total-size-calculating",
+ args: null,
+ },
+ "Should show the loading message while updating"
+ );
+
+ Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+ is(
+ clearBtn.disabled,
+ false,
+ "Should enable clear button after sites updated"
+ );
+ is(
+ settingsButton.disabled,
+ false,
+ "Should enable settings button after sites updated"
+ );
+ cacheSize = await SiteDataManager.getCacheSize();
+ await SiteDataManager.getTotalUsage().then(usage => {
+ let [value, unit] = DownloadUtils.convertByteUnits(usage + cacheSize);
+ Assert.deepEqual(
+ doc.l10n.getAttributes(totalSiteDataSizeLabel),
+ {
+ id: "sitedata-total-size",
+ args: { value, unit },
+ },
+ "Should show the right total site data size"
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test clearing service worker through the settings panel
+add_task(async function () {
+ // Register a test service worker
+ await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ // Test the initial states
+ await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+ // Open the Site Data Settings panel and remove the site
+ await openSiteDataSettingsDialog();
+ let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ TEST_OFFLINE_HOST }],
+ args => {
+ let host = args.TEST_OFFLINE_HOST;
+ let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ let sitesList = frameDoc.getElementById("sitesList");
+ let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+ if (site) {
+ let removeBtn = frameDoc.getElementById("removeSelected");
+ let saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ site.click();
+ removeBtn.doCommand();
+ saveBtn.doCommand();
+ } else {
+ ok(false, `Should have one site of ${host}`);
+ }
+ }
+ );
+ await acceptRemovePromise;
+ await updatePromise;
+ await promiseServiceWorkersCleared();
+ await SiteDataManager.removeAll();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test showing and removing sites with cookies.
+add_task(async function () {
+ // Add some test cookies.
+ let uri = Services.io.newURI("https://example.com");
+ let uri2 = Services.io.newURI("https://example.org");
+ Services.cookies.add(
+ uri.host,
+ uri.pathQueryRef,
+ "test1",
+ "1",
+ false,
+ false,
+ false,
+ Date.now() + 1000 * 60 * 60,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Services.cookies.add(
+ uri.host,
+ uri.pathQueryRef,
+ "test2",
+ "2",
+ false,
+ false,
+ false,
+ Date.now() + 1000 * 60 * 60,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Services.cookies.add(
+ uri2.host,
+ uri2.pathQueryRef,
+ "test1",
+ "1",
+ false,
+ false,
+ false,
+ Date.now() + 1000 * 60 * 60,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+
+ // Ensure that private browsing cookies are ignored.
+ Services.cookies.add(
+ uri.host,
+ uri.pathQueryRef,
+ "test3",
+ "3",
+ false,
+ false,
+ false,
+ Date.now() + 1000 * 60 * 60,
+ { privateBrowsingId: 1 },
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+
+ // Get the exact creation date from the cookies (to avoid intermittents
+ // from minimal time differences, since we round up to minutes).
+ let cookies1 = Services.cookies.getCookiesFromHost(uri.host, {});
+ let cookies2 = Services.cookies.getCookiesFromHost(uri2.host, {});
+ // We made two valid cookies for example.com.
+ let cookie1 = cookies1[1];
+ let cookie2 = cookies2[0];
+
+ let fullFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "short",
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+
+ // Open the site data manager and remove one site.
+ await openSiteDataSettingsDialog();
+ let creationDate1 = new Date(cookie1.lastAccessed / 1000);
+ let creationDate1Formatted = fullFormatter.format(creationDate1);
+ let creationDate2 = new Date(cookie2.lastAccessed / 1000);
+ let creationDate2Formatted = fullFormatter.format(creationDate2);
+ let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [
+ {
+ creationDate1Formatted,
+ creationDate2Formatted,
+ },
+ ],
+ function (args) {
+ let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+
+ let siteItems = frameDoc.getElementsByTagName("richlistitem");
+ is(siteItems.length, 2, "Should list two sites with cookies");
+ let sitesList = frameDoc.getElementById("sitesList");
+ let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+ let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
+
+ let columns = site1.querySelectorAll(".item-box > label");
+ let boxes = site1.querySelectorAll(".item-box");
+ is(columns[0].value, "example.com", "Should show the correct host.");
+ is(columns[1].value, "2", "Should show the correct number of cookies.");
+ is(columns[2].value, "", "Should show no site data.");
+ is(
+ /(now|second)/.test(columns[3].value),
+ true,
+ "Should show the relative date."
+ );
+ is(
+ boxes[3].getAttribute("tooltiptext"),
+ args.creationDate1Formatted,
+ "Should show the correct date."
+ );
+
+ columns = site2.querySelectorAll(".item-box > label");
+ boxes = site2.querySelectorAll(".item-box");
+ is(columns[0].value, "example.org", "Should show the correct host.");
+ is(columns[1].value, "1", "Should show the correct number of cookies.");
+ is(columns[2].value, "", "Should show no site data.");
+ is(
+ /(now|second)/.test(columns[3].value),
+ true,
+ "Should show the relative date."
+ );
+ is(
+ boxes[3].getAttribute("tooltiptext"),
+ args.creationDate2Formatted,
+ "Should show the correct date."
+ );
+
+ let removeBtn = frameDoc.getElementById("removeSelected");
+ let saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ site2.click();
+ removeBtn.doCommand();
+ saveBtn.doCommand();
+ }
+ );
+ await removeDialogOpenPromise;
+
+ await TestUtils.waitForCondition(
+ () => Services.cookies.countCookiesFromHost(uri2.host) == 0,
+ "Cookies from the first host should be cleared"
+ );
+ is(
+ Services.cookies.countCookiesFromHost(uri.host),
+ 2,
+ "Cookies from the second host should not be cleared"
+ );
+
+ // Open the site data manager and remove another site.
+ await openSiteDataSettingsDialog();
+ let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ creationDate1Formatted }],
+ function (args) {
+ let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+
+ let siteItems = frameDoc.getElementsByTagName("richlistitem");
+ is(siteItems.length, 1, "Should list one site with cookies");
+ let sitesList = frameDoc.getElementById("sitesList");
+ let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+
+ let columns = site1.querySelectorAll(".item-box > label");
+ let boxes = site1.querySelectorAll(".item-box");
+ is(columns[0].value, "example.com", "Should show the correct host.");
+ is(columns[1].value, "2", "Should show the correct number of cookies.");
+ is(columns[2].value, "", "Should show no site data.");
+ is(
+ /(now|second)/.test(columns[3].value),
+ true,
+ "Should show the relative date."
+ );
+ is(
+ boxes[3].getAttribute("tooltiptext"),
+ args.creationDate1Formatted,
+ "Should show the correct date."
+ );
+
+ let removeBtn = frameDoc.getElementById("removeSelected");
+ let saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ site1.click();
+ removeBtn.doCommand();
+ saveBtn.doCommand();
+ }
+ );
+ await acceptRemovePromise;
+
+ await TestUtils.waitForCondition(
+ () => Services.cookies.countCookiesFromHost(uri.host) == 0,
+ "Cookies from the second host should be cleared"
+ );
+
+ await openSiteDataSettingsDialog();
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+
+ let siteItems = frameDoc.getElementsByTagName("richlistitem");
+ is(siteItems.length, 0, "Should list no sites with cookies");
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/siteData/browser_siteData2.js b/browser/components/preferences/tests/siteData/browser_siteData2.js
new file mode 100644
index 0000000000..863a8dcefe
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/browser_siteData2.js
@@ -0,0 +1,475 @@
+"use strict";
+
+function assertAllSitesNotListed(win) {
+ let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ let removeBtn = frameDoc.getElementById("removeSelected");
+ let removeAllBtn = frameDoc.getElementById("removeAll");
+ let sitesList = frameDoc.getElementById("sitesList");
+ let sites = sitesList.getElementsByTagName("richlistitem");
+ is(sites.length, 0, "Should not list all sites");
+ is(removeBtn.disabled, true, "Should disable the removeSelected button");
+ is(removeAllBtn.disabled, true, "Should disable the removeAllBtn button");
+}
+
+// Test selecting and removing all sites one by one
+add_task(async function test_selectRemove() {
+ let hosts = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "https://shopping.xyz.com",
+ },
+ {
+ usage: 1024,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "http://email.bar.com",
+ },
+ ]);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let frameDoc = null;
+ let saveBtn = null;
+ let cancelBtn = null;
+ let settingsDialogClosePromise = null;
+
+ // Test the initial state
+ assertSitesListed(doc, hosts);
+
+ // Test the "Cancel" button
+ settingsDialogClosePromise = promiseSettingsDialogClose();
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ cancelBtn = frameDoc.querySelector("dialog").getButton("cancel");
+ removeAllSitesOneByOne();
+ assertAllSitesNotListed(win);
+ cancelBtn.doCommand();
+ await settingsDialogClosePromise;
+ await openSiteDataSettingsDialog();
+ assertSitesListed(doc, hosts);
+
+ // Test the "Save Changes" button but cancelling save
+ let cancelPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel");
+ settingsDialogClosePromise = promiseSettingsDialogClose();
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ cancelBtn = frameDoc.querySelector("dialog").getButton("cancel");
+ removeAllSitesOneByOne();
+ assertAllSitesNotListed(win);
+ saveBtn.doCommand();
+ await cancelPromise;
+ cancelBtn.doCommand();
+ await settingsDialogClosePromise;
+ await openSiteDataSettingsDialog();
+ assertSitesListed(doc, hosts);
+
+ // Test the "Save Changes" button and accepting save
+ let acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+ settingsDialogClosePromise = promiseSettingsDialogClose();
+ updatePromise = promiseSiteDataManagerSitesUpdated();
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ removeAllSitesOneByOne();
+ assertAllSitesNotListed(win);
+ saveBtn.doCommand();
+ await acceptPromise;
+ await settingsDialogClosePromise;
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+ assertAllSitesNotListed(win);
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ function removeAllSitesOneByOne() {
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ let removeBtn = frameDoc.getElementById("removeSelected");
+ let sitesList = frameDoc.getElementById("sitesList");
+ let sites = sitesList.getElementsByTagName("richlistitem");
+ for (let i = sites.length - 1; i >= 0; --i) {
+ sites[i].click();
+ removeBtn.doCommand();
+ }
+ }
+});
+
+// Test selecting and removing partial sites
+add_task(async function test_removePartialSites() {
+ let hosts = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "https://shopping.xyz.com",
+ persisted: false,
+ },
+ {
+ usage: 1024,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "http://email.bar.com",
+ persisted: false,
+ },
+ {
+ usage: 1024,
+ origin: "https://s3-us-west-2.amazonaws.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "https://127.0.0.1",
+ persisted: false,
+ },
+ ]);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let frameDoc = null;
+ let saveBtn = null;
+ let cancelBtn = null;
+ let removeDialogOpenPromise = null;
+ let settingsDialogClosePromise = null;
+
+ // Test the initial state
+ assertSitesListed(doc, hosts);
+
+ // Test the "Cancel" button
+ settingsDialogClosePromise = promiseSettingsDialogClose();
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ cancelBtn = frameDoc.querySelector("dialog").getButton("cancel");
+ await removeSelectedSite(hosts.slice(0, 2));
+ assertSitesListed(doc, hosts.slice(2));
+ cancelBtn.doCommand();
+ await settingsDialogClosePromise;
+ await openSiteDataSettingsDialog();
+ assertSitesListed(doc, hosts);
+
+ // Test the "Save Changes" button but canceling save
+ removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "cancel",
+ REMOVE_DIALOG_URL
+ );
+ settingsDialogClosePromise = promiseSettingsDialogClose();
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ cancelBtn = frameDoc.querySelector("dialog").getButton("cancel");
+ await removeSelectedSite(hosts.slice(0, 2));
+ assertSitesListed(doc, hosts.slice(2));
+ saveBtn.doCommand();
+ await removeDialogOpenPromise;
+ cancelBtn.doCommand();
+ await settingsDialogClosePromise;
+ await openSiteDataSettingsDialog();
+ assertSitesListed(doc, hosts);
+
+ // Test the "Save Changes" button and accepting save
+ removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ settingsDialogClosePromise = promiseSettingsDialogClose();
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ await removeSelectedSite(hosts.slice(0, 2));
+ assertSitesListed(doc, hosts.slice(2));
+ saveBtn.doCommand();
+ await removeDialogOpenPromise;
+ await settingsDialogClosePromise;
+ await openSiteDataSettingsDialog();
+ assertSitesListed(doc, hosts.slice(2));
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ function removeSelectedSite(removeHosts) {
+ frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ let removeBtn = frameDoc.getElementById("removeSelected");
+ is(
+ removeBtn.disabled,
+ true,
+ "Should start with disabled removeSelected button"
+ );
+ let sitesList = frameDoc.getElementById("sitesList");
+ removeHosts.forEach(host => {
+ let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+ if (site) {
+ site.click();
+ let currentSelectedIndex = sitesList.selectedIndex;
+ is(
+ removeBtn.disabled,
+ false,
+ "Should enable the removeSelected button"
+ );
+ removeBtn.doCommand();
+ let newSelectedIndex = sitesList.selectedIndex;
+ if (currentSelectedIndex >= sitesList.itemCount) {
+ is(newSelectedIndex, currentSelectedIndex - 1);
+ } else {
+ is(newSelectedIndex, currentSelectedIndex);
+ }
+ } else {
+ ok(false, `Should not select and remove inexistent site of ${host}`);
+ }
+ });
+ }
+});
+
+// Test searching and then removing only visible sites
+add_task(async function () {
+ let hosts = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "https://shopping.xyz.com",
+ persisted: false,
+ },
+ {
+ usage: 1024,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "http://email.bar.com",
+ persisted: false,
+ },
+ ]);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ // Search "foo" to only list foo.com sites
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ let searchBox = frameDoc.getElementById("searchBox");
+ searchBox.value = "xyz";
+ searchBox.doCommand();
+ assertSitesListed(
+ doc,
+ hosts.filter(host => host.includes("xyz"))
+ );
+
+ // Test only removing all visible sites listed
+ updatePromise = promiseSiteDataManagerSitesUpdated();
+ let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ let settingsDialogClosePromise = promiseSettingsDialogClose();
+ let removeAllBtn = frameDoc.getElementById("removeAll");
+ let saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ removeAllBtn.doCommand();
+ saveBtn.doCommand();
+ await acceptRemovePromise;
+ await settingsDialogClosePromise;
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+ assertSitesListed(
+ doc,
+ hosts.filter(host => !host.includes("xyz"))
+ );
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test dynamically clearing all site data
+add_task(async function () {
+ let hosts = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "https://shopping.xyz.com",
+ persisted: false,
+ },
+ ]);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+
+ // Test the initial state
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ assertSitesListed(doc, hosts);
+
+ await addTestData([
+ {
+ usage: 1024,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "http://email.bar.com",
+ persisted: false,
+ },
+ ]);
+
+ // Test clearing all site data dynamically
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ updatePromise = promiseSiteDataManagerSitesUpdated();
+ let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+ let settingsDialogClosePromise = promiseSettingsDialogClose();
+ let removeAllBtn = frameDoc.getElementById("removeAll");
+ let saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ removeAllBtn.doCommand();
+ saveBtn.doCommand();
+ await acceptRemovePromise;
+ await settingsDialogClosePromise;
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+ assertAllSitesNotListed(win);
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Tests clearing search box content via backspace does not delete site data
+add_task(async function () {
+ let hosts = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "https://shopping.xyz.com",
+ persisted: false,
+ },
+ {
+ usage: 1024,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "http://email.bar.com",
+ persisted: false,
+ },
+ ]);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await openSiteDataSettingsDialog();
+
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ let searchBox = frameDoc.getElementById("searchBox");
+ searchBox.value = "xyz";
+ searchBox.doCommand();
+ assertSitesListed(
+ doc,
+ hosts.filter(host => host.includes("xyz"))
+ );
+
+ // Make sure the focus is on the search box
+ searchBox.focus();
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ } else {
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ }
+ assertSitesListed(
+ doc,
+ hosts.filter(host => host.includes("xyz"))
+ );
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Tests remove site data via backspace
+add_task(async function () {
+ let hosts = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "https://shopping.xyz.com",
+ persisted: false,
+ },
+ {
+ usage: 1024,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "http://email.bar.com",
+ persisted: false,
+ },
+ ]);
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await openSiteDataSettingsDialog();
+
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ // Test initial state
+ assertSitesListed(doc, hosts);
+
+ let sitesList = frameDoc.getElementById("sitesList");
+ let site = sitesList.querySelector(`richlistitem[host="xyz.com"]`);
+ if (site) {
+ // Move the focus from the search box to the list and select an item
+ sitesList.focus();
+ site.click();
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ } else {
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ }
+ }
+
+ assertSitesListed(
+ doc,
+ hosts.filter(host => !host.includes("xyz"))
+ );
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/siteData/browser_siteData3.js b/browser/components/preferences/tests/siteData/browser_siteData3.js
new file mode 100644
index 0000000000..58aa5bf1b9
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/browser_siteData3.js
@@ -0,0 +1,327 @@
+"use strict";
+
+// Test not displaying sites which store 0 byte and don't have persistent storage.
+add_task(async function test_exclusions() {
+ let hosts = await addTestData([
+ {
+ usage: 0,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 0,
+ origin: "https://shopping.xyz.com",
+ persisted: false,
+ },
+ {
+ usage: 1024,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024,
+ origin: "http://email.bar.com",
+ persisted: false,
+ },
+ {
+ usage: 0,
+ origin: "http://cookies.bar.com",
+ cookies: 5,
+ persisted: false,
+ },
+ ]);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+ assertSitesListed(
+ doc,
+ hosts.filter(host => host != "shopping.xyz.com")
+ );
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test grouping and listing sites across scheme, port and origin attributes by base domain.
+add_task(async function test_grouping() {
+ let quotaUsage = 7000000;
+ let testData = [
+ {
+ usage: quotaUsage,
+ origin: "https://account.xyz.com^userContextId=1",
+ cookies: 2,
+ persisted: true,
+ },
+ {
+ usage: quotaUsage,
+ origin: "https://account.xyz.com",
+ cookies: 1,
+ persisted: false,
+ },
+ {
+ usage: quotaUsage,
+ origin: "https://account.xyz.com:123",
+ cookies: 1,
+ persisted: false,
+ },
+ {
+ usage: quotaUsage,
+ origin: "http://account.xyz.com",
+ cookies: 1,
+ persisted: false,
+ },
+ {
+ usage: quotaUsage,
+ origin: "http://search.xyz.com",
+ cookies: 3,
+ persisted: false,
+ },
+ {
+ usage: quotaUsage,
+ origin: "http://advanced.search.xyz.com",
+ cookies: 3,
+ persisted: true,
+ },
+ {
+ usage: quotaUsage,
+ origin: "http://xyz.com",
+ cookies: 1,
+ persisted: false,
+ },
+ ];
+ await addTestData(testData);
+
+ let updatedPromise = promiseSiteDataManagerSitesUpdated();
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatedPromise;
+ await openSiteDataSettingsDialog();
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let dialogFrame = win.gSubDialog._topDialog._frame;
+ let frameDoc = dialogFrame.contentDocument;
+
+ let siteItems = frameDoc.getElementsByTagName("richlistitem");
+ is(
+ siteItems.length,
+ 1,
+ "Should group sites across scheme, port and origin attributes"
+ );
+
+ let columns = siteItems[0].querySelectorAll(".item-box > label");
+
+ let expected = "xyz.com";
+ is(columns[0].value, expected, "Should group and list sites by host");
+
+ let cookieCount = testData.reduce((count, { cookies }) => count + cookies, 0);
+ is(
+ columns[1].value,
+ cookieCount.toString(),
+ "Should group cookies across scheme, port and origin attributes"
+ );
+
+ let [value, unit] = DownloadUtils.convertByteUnits(quotaUsage * 4);
+ let l10nAttributes = frameDoc.l10n.getAttributes(columns[2]);
+ is(
+ l10nAttributes.id,
+ "site-storage-persistent",
+ "Should show the site as persistent if one origin is persistent."
+ );
+ // The shown quota can be slightly larger than the raw data we put in (though it should
+ // never be smaller), but that doesn't really matter to us since we only want to test that
+ // the site data dialog accumulates this into a single column.
+ ok(
+ parseFloat(l10nAttributes.args.value) >= parseFloat(value),
+ "Should show the correct accumulated quota size."
+ );
+ is(
+ l10nAttributes.args.unit,
+ unit,
+ "Should show the correct quota size unit."
+ );
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test sorting
+add_task(async function test_sorting() {
+ let testData = [
+ {
+ usage: 1024,
+ origin: "https://account.xyz.com",
+ cookies: 6,
+ persisted: true,
+ },
+ {
+ usage: 1024 * 2,
+ origin: "https://books.foo.com",
+ cookies: 0,
+ persisted: false,
+ },
+ {
+ usage: 1024 * 3,
+ origin: "http://cinema.bar.com",
+ cookies: 3,
+ persisted: true,
+ },
+ {
+ usage: 1024 * 3,
+ origin: "http://vod.bar.com",
+ cookies: 2,
+ persisted: false,
+ },
+ ];
+
+ await addTestData(testData);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ let dialog = content.gSubDialog._topDialog;
+ let dialogFrame = dialog._frame;
+ let frameDoc = dialogFrame.contentDocument;
+ let hostCol = frameDoc.getElementById("hostCol");
+ let usageCol = frameDoc.getElementById("usageCol");
+ let cookiesCol = frameDoc.getElementById("cookiesCol");
+ let sitesList = frameDoc.getElementById("sitesList");
+
+ function getHostOrder() {
+ let siteItems = sitesList.getElementsByTagName("richlistitem");
+ return Array.from(siteItems).map(item => item.getAttribute("host"));
+ }
+
+ // Test default sorting by usage, descending.
+ Assert.deepEqual(
+ getHostOrder(),
+ ["bar.com", "foo.com", "xyz.com"],
+ "Has sorted descending by usage"
+ );
+
+ // Test sorting on the usage column
+ usageCol.click();
+ Assert.deepEqual(
+ getHostOrder(),
+ ["xyz.com", "foo.com", "bar.com"],
+ "Has sorted ascending by usage"
+ );
+ usageCol.click();
+ Assert.deepEqual(
+ getHostOrder(),
+ ["bar.com", "foo.com", "xyz.com"],
+ "Has sorted descending by usage"
+ );
+
+ // Test sorting on the host column
+ hostCol.click();
+ Assert.deepEqual(
+ getHostOrder(),
+ ["bar.com", "foo.com", "xyz.com"],
+ "Has sorted ascending by base domain"
+ );
+ hostCol.click();
+ Assert.deepEqual(
+ getHostOrder(),
+ ["xyz.com", "foo.com", "bar.com"],
+ "Has sorted descending by base domain"
+ );
+
+ // Test sorting on the cookies column
+ cookiesCol.click();
+ Assert.deepEqual(
+ getHostOrder(),
+ ["foo.com", "bar.com", "xyz.com"],
+ "Has sorted ascending by cookies"
+ );
+ cookiesCol.click();
+ Assert.deepEqual(
+ getHostOrder(),
+ ["xyz.com", "bar.com", "foo.com"],
+ "Has sorted descending by cookies"
+ );
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Test single entry removal
+add_task(async function test_single_entry_removal() {
+ let testData = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://xyz.com",
+ cookies: 6,
+ persisted: true,
+ },
+ {
+ usage: 1024 * 3,
+ origin: "http://bar.com",
+ cookies: 2,
+ persisted: false,
+ },
+ ]);
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ let dialog = content.gSubDialog._topDialog;
+ let dialogFrame = dialog._frame;
+ let frameDoc = dialogFrame.contentDocument;
+
+ let sitesList = frameDoc.getElementById("sitesList");
+ let host = testData[0];
+ let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+ sitesList.addItemToSelection(site);
+ frameDoc.getElementById("removeSelected").doCommand();
+ let saveChangesButton = frameDoc.querySelector("dialog").getButton("accept");
+ let dialogOpened = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ REMOVE_DIALOG_URL
+ );
+ setTimeout(() => saveChangesButton.doCommand(), 0);
+ let dialogWin = await dialogOpened;
+ let rootElement = dialogWin.document.getElementById(
+ "SiteDataRemoveSelectedDialog"
+ );
+ is(rootElement.classList.length, 1, "There should only be one class set");
+ is(
+ rootElement.classList[0],
+ "single-entry",
+ "The only class set should be single-entry (to hide the list)"
+ );
+ let description = dialogWin.document.getElementById("removing-description");
+ is(
+ description.getAttribute("data-l10n-id"),
+ "site-data-removing-single-desc",
+ "The description for single site should be selected"
+ );
+
+ let removalList = dialogWin.document.getElementById("removalList");
+ is(
+ BrowserTestUtils.is_visible(removalList),
+ false,
+ "The removal list should be invisible"
+ );
+ let removeButton = dialogWin.document
+ .querySelector("dialog")
+ .getButton("accept");
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+ updatePromise = promiseSiteDataManagerSitesUpdated();
+ removeButton.doCommand();
+ await dialogClosed;
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ dialog = content.gSubDialog._topDialog;
+ dialogFrame = dialog._frame;
+ frameDoc = dialogFrame.contentDocument;
+ assertSitesListed(frameDoc, testData.slice(1));
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js b/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js
new file mode 100644
index 0000000000..5ce9d7e1e1
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js
@@ -0,0 +1,119 @@
+"use strict";
+
+// Test selecting and removing partial sites
+add_task(async function () {
+ await SiteDataTestUtils.clear();
+
+ let hosts = await addTestData([
+ {
+ usage: 1024,
+ origin: "https://127.0.0.1",
+ persisted: false,
+ },
+ {
+ usage: 1024 * 4,
+ origin: "http://cinema.bar.com",
+ persisted: true,
+ },
+ {
+ usage: 1024 * 3,
+ origin: "http://email.bar.com",
+ persisted: false,
+ },
+ {
+ usage: 1024 * 2,
+ origin: "https://s3-us-west-2.amazonaws.com",
+ persisted: true,
+ },
+ {
+ usage: 1024 * 6,
+ origin: "https://account.xyz.com",
+ persisted: true,
+ },
+ {
+ usage: 1024 * 5,
+ origin: "https://shopping.xyz.com",
+ persisted: false,
+ },
+ {
+ usage: 1024 * 5,
+ origin: "https://example.com",
+ persisted: false,
+ },
+ {
+ usage: 1024 * 5,
+ origin: "https://example.net",
+ persisted: false,
+ },
+ ]);
+
+ // Align the order of test hosts with the order of the site data table.
+ hosts.sort();
+
+ let updatePromise = promiseSiteDataManagerSitesUpdated();
+ await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ let doc = gBrowser.selectedBrowser.contentDocument;
+
+ // Test the initial state
+ assertSitesListed(doc, hosts);
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+ let removeBtn = frameDoc.getElementById("removeSelected");
+ is(
+ removeBtn.disabled,
+ true,
+ "Should start with disabled removeSelected button"
+ );
+
+ let hostCol = frameDoc.getElementById("hostCol");
+ hostCol.click();
+
+ let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ "accept",
+ REMOVE_DIALOG_URL
+ );
+ let settingsDialogClosePromise = promiseSettingsDialogClose();
+
+ // Select some sites to remove.
+ let sitesList = frameDoc.getElementById("sitesList");
+ hosts.slice(0, 2).forEach(host => {
+ let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+ sitesList.addItemToSelection(site);
+ });
+
+ is(removeBtn.disabled, false, "Should enable the removeSelected button");
+ removeBtn.doCommand();
+ is(sitesList.selectedIndex, 0, "Should select next item");
+ assertSitesListed(doc, hosts.slice(2));
+
+ // Select some other sites to remove with Delete.
+ hosts.slice(2, 4).forEach(host => {
+ let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+ sitesList.addItemToSelection(site);
+ });
+
+ is(removeBtn.disabled, false, "Should enable the removeSelected button");
+ // Move the focus from the search box to the list
+ sitesList.focus();
+ EventUtils.synthesizeKey("VK_DELETE");
+ is(sitesList.selectedIndex, 0, "Should select next item");
+ assertSitesListed(doc, hosts.slice(4));
+
+ updatePromise = promiseSiteDataManagerSitesUpdated();
+ let saveBtn = frameDoc.querySelector("dialog").getButton("accept");
+ saveBtn.doCommand();
+
+ await removeDialogOpenPromise;
+ await settingsDialogClosePromise;
+
+ await updatePromise;
+ await openSiteDataSettingsDialog();
+
+ assertSitesListed(doc, hosts.slice(4));
+
+ await SiteDataTestUtils.clear();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/preferences/tests/siteData/head.js b/browser/components/preferences/tests/siteData/head.js
new file mode 100644
index 0000000000..01f56d879d
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/head.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_QUOTA_USAGE_HOST = "example.com";
+const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+const TEST_QUOTA_USAGE_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_QUOTA_USAGE_ORIGIN
+ ) + "/site_data_test.html";
+const TEST_OFFLINE_HOST = "example.org";
+const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+const TEST_OFFLINE_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_OFFLINE_ORIGIN
+ ) + "/offline/offline.html";
+const TEST_SERVICE_WORKER_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_OFFLINE_ORIGIN
+ ) + "/service_worker_test.html";
+
+const REMOVE_DIALOG_URL =
+ "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "serviceWorkerManager",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager"
+);
+
+function promiseSiteDataManagerSitesUpdated() {
+ return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+}
+
+function is_element_visible(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(!BrowserTestUtils.is_hidden(aElement), aMsg);
+}
+
+function is_element_hidden(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(BrowserTestUtils.is_hidden(aElement), aMsg);
+}
+
+function promiseLoadSubDialog(aURL) {
+ return new Promise((resolve, reject) => {
+ content.gSubDialog._dialogStack.addEventListener(
+ "dialogopen",
+ function dialogopen(aEvent) {
+ if (
+ aEvent.detail.dialog._frame.contentWindow.location == "about:blank"
+ ) {
+ return;
+ }
+ content.gSubDialog._dialogStack.removeEventListener(
+ "dialogopen",
+ dialogopen
+ );
+
+ is(
+ aEvent.detail.dialog._frame.contentWindow.location.toString(),
+ aURL,
+ "Check the proper URL is loaded"
+ );
+
+ // Check visibility
+ is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible");
+
+ // Check that stylesheets were injected
+ let expectedStyleSheetURLs =
+ aEvent.detail.dialog._injectedStyleSheets.slice(0);
+ for (let styleSheet of aEvent.detail.dialog._frame.contentDocument
+ .styleSheets) {
+ let i = expectedStyleSheetURLs.indexOf(styleSheet.href);
+ if (i >= 0) {
+ info("found " + styleSheet.href);
+ expectedStyleSheetURLs.splice(i, 1);
+ }
+ }
+ is(
+ expectedStyleSheetURLs.length,
+ 0,
+ "All expectedStyleSheetURLs should have been found"
+ );
+
+ // Wait for the next event tick to make sure the remaining part of the
+ // testcase runs after the dialog gets ready for input.
+ executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow));
+ }
+ );
+ });
+}
+
+function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) {
+ return new Promise(resolve => {
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ openPreferences(aPane);
+ let newTabBrowser = gBrowser.selectedBrowser;
+
+ newTabBrowser.addEventListener(
+ "Initialized",
+ function () {
+ newTabBrowser.contentWindow.addEventListener(
+ "load",
+ async function () {
+ let win = gBrowser.contentWindow;
+ let selectedPane = win.history.state;
+ await finalPrefPaneLoaded;
+ if (!aOptions || !aOptions.leaveOpen) {
+ gBrowser.removeCurrentTab();
+ }
+ resolve({ selectedPane });
+ },
+ { once: true }
+ );
+ },
+ { capture: true, once: true }
+ );
+ });
+}
+
+function openSiteDataSettingsDialog() {
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let settingsBtn = doc.getElementById("siteDataSettings");
+ let dialogOverlay = content.gSubDialog._preloadDialog._overlay;
+ let dialogLoadPromise = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml"
+ );
+ let dialogInitPromise = TestUtils.topicObserved(
+ "sitedata-settings-init",
+ () => true
+ );
+ let fullyLoadPromise = Promise.all([
+ dialogLoadPromise,
+ dialogInitPromise,
+ ]).then(() => {
+ is_element_visible(dialogOverlay, "The Settings dialog should be visible");
+ });
+ settingsBtn.doCommand();
+ return fullyLoadPromise;
+}
+
+function promiseSettingsDialogClose() {
+ return new Promise(resolve => {
+ let win = gBrowser.selectedBrowser.contentWindow;
+ let dialogOverlay = win.gSubDialog._topDialog._overlay;
+ let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
+ dialogWin.addEventListener(
+ "unload",
+ function unload() {
+ if (
+ dialogWin.document.documentURI ===
+ "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml"
+ ) {
+ is_element_hidden(
+ dialogOverlay,
+ "The Settings dialog should be hidden"
+ );
+ resolve();
+ }
+ },
+ { once: true }
+ );
+ });
+}
+
+function assertSitesListed(doc, hosts) {
+ let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ let removeAllBtn = frameDoc.getElementById("removeAll");
+ let sitesList = frameDoc.getElementById("sitesList");
+ let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+ is(totalSitesNumber, hosts.length, "Should list the right sites number");
+ hosts.forEach(host => {
+ let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+ ok(site, `Should list the site of ${host}`);
+ });
+ is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+}
+
+// Counter used by addTestData to generate unique cookie names across function
+// calls.
+let cookieID = 0;
+
+async function addTestData(data) {
+ let hosts = new Set();
+
+ for (let site of data) {
+ is(
+ typeof site.origin,
+ "string",
+ "Passed an origin string into addTestData."
+ );
+ if (site.persisted) {
+ await SiteDataTestUtils.persist(site.origin);
+ }
+
+ if (site.usage) {
+ await SiteDataTestUtils.addToIndexedDB(site.origin, site.usage);
+ }
+
+ for (let i = 0; i < (site.cookies || 0); i++) {
+ SiteDataTestUtils.addToCookies({
+ origin: site.origin,
+ name: `cookie${cookieID++}`,
+ });
+ }
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ site.origin
+ );
+
+ hosts.add(principal.baseDomain || principal.host);
+ }
+
+ return Array.from(hosts);
+}
+
+function promiseCookiesCleared() {
+ return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+ return data === "cleared";
+ });
+}
+
+async function loadServiceWorkerTestPage(url) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await TestUtils.waitForCondition(() => {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () =>
+ content.document.body.getAttribute(
+ "data-test-service-worker-registered"
+ ) === "true"
+ );
+ }, `Fail to load service worker test ${url}`);
+ BrowserTestUtils.removeTab(tab);
+}
+
+function promiseServiceWorkersCleared() {
+ return TestUtils.waitForCondition(() => {
+ let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+ if (!serviceWorkers.length) {
+ ok(true, "Cleared all service workers");
+ return true;
+ }
+ return false;
+ }, "Should clear all service workers");
+}
+
+function promiseServiceWorkerRegisteredFor(url) {
+ return TestUtils.waitForCondition(() => {
+ try {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(url);
+ let sw = serviceWorkerManager.getRegistrationByPrincipal(
+ principal,
+ principal.spec
+ );
+ if (sw) {
+ ok(true, `Found the service worker registered for ${url}`);
+ return true;
+ }
+ } catch (e) {}
+ return false;
+ }, `Should register service worker for ${url}`);
+}
diff --git a/browser/components/preferences/tests/siteData/offline/manifest.appcache b/browser/components/preferences/tests/siteData/offline/manifest.appcache
new file mode 100644
index 0000000000..a9287c64e6
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/offline/manifest.appcache
@@ -0,0 +1,3 @@
+CACHE MANIFEST
+# V1
+offline.html
diff --git a/browser/components/preferences/tests/siteData/offline/offline.html b/browser/components/preferences/tests/siteData/offline/offline.html
new file mode 100644
index 0000000000..f76b8a2bce
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/offline/offline.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html manifest="manifest.appcache">>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="public" />
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
+
+ </head>
+
+ <body>
+ <h1>Set up offline appcache Test</h1>
+ </body>
+</html>
diff --git a/browser/components/preferences/tests/siteData/service_worker_test.html b/browser/components/preferences/tests/siteData/service_worker_test.html
new file mode 100644
index 0000000000..56f5173481
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/service_worker_test.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="public" />
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
+
+ <title>Service Worker Test</title>
+
+ </head>
+
+ <body>
+ <h1>Service Worker Test</h1>
+ <script type="text/javascript">
+ navigator.serviceWorker.register("service_worker_test.js")
+ .then(regis => document.body.setAttribute("data-test-service-worker-registered", "true"));
+ </script>
+ </body>
+</html>
diff --git a/browser/components/preferences/tests/siteData/service_worker_test.js b/browser/components/preferences/tests/siteData/service_worker_test.js
new file mode 100644
index 0000000000..2aba167d18
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/service_worker_test.js
@@ -0,0 +1 @@
+// empty worker, always succeed!
diff --git a/browser/components/preferences/tests/siteData/site_data_test.html b/browser/components/preferences/tests/siteData/site_data_test.html
new file mode 100644
index 0000000000..758106b0a5
--- /dev/null
+++ b/browser/components/preferences/tests/siteData/site_data_test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Cache-Control" content="public" />
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
+
+ <title>Site Data Test</title>
+
+ </head>
+
+ <body>
+ <h1>Site Data Test</h1>
+ <script type="text/javascript">
+ let request = indexedDB.open("TestDatabase", 1);
+ request.onupgradeneeded = function(e) {
+ let db = e.target.result;
+ db.createObjectStore("TestStore", { keyPath: "id" });
+ };
+ request.onsuccess = function(e) {
+ let db = e.target.result;
+ let tx = db.transaction("TestStore", "readwrite");
+ let store = tx.objectStore("TestStore");
+ tx.oncomplete = () => document.dispatchEvent(new CustomEvent("test-indexedDB-done", {bubbles: true, cancelable: false}));
+ store.put({ id: "test_id", description: "Site Data Test"});
+ };
+ </script>
+ </body>
+</html>
diff --git a/browser/components/preferences/tests/subdialog.xhtml b/browser/components/preferences/tests/subdialog.xhtml
new file mode 100644
index 0000000000..54fa88c25d
--- /dev/null
+++ b/browser/components/preferences/tests/subdialog.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Sample sub-dialog" style="width: 32em; height: 5em;"
+ onload="document.getElementById('textbox').focus();">
+<dialog id="subDialog">
+ <script>
+ document.addEventListener("dialogaccept", acceptSubdialog);
+ function acceptSubdialog() {
+ window.arguments[0].acceptCount++;
+ }
+ </script>
+
+ <description id="desc">A sample sub-dialog for testing</description>
+
+ <html:input id="textbox" value="Default text" />
+
+ <separator class="thin"/>
+
+ <button oncommand="window.close();" label="Close" />
+
+</dialog>
+</window>
diff --git a/browser/components/preferences/tests/subdialog2.xhtml b/browser/components/preferences/tests/subdialog2.xhtml
new file mode 100644
index 0000000000..9ae04d5675
--- /dev/null
+++ b/browser/components/preferences/tests/subdialog2.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Sample sub-dialog #2" style="width: 32em; height: 5em;"
+ onload="document.getElementById('textbox').focus();">
+<dialog id="subDialog">
+ <script>
+ document.addEventListener("dialogaccept", acceptSubdialog);
+ function acceptSubdialog() {
+ window.arguments[0].acceptCount++;
+ }
+ </script>
+
+ <description id="desc">A sample sub-dialog for testing</description>
+
+ <html:input id="textbox" value="Default text" />
+
+ <separator class="thin"/>
+
+ <button oncommand="window.close();" label="Close" />
+
+</dialog>
+</window>
diff --git a/browser/components/preferences/web-appearance-dark.svg b/browser/components/preferences/web-appearance-dark.svg
new file mode 100644
index 0000000000..1f4c1d81c2
--- /dev/null
+++ b/browser/components/preferences/web-appearance-dark.svg
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg viewBox="0 0 54 42" xmlns="http://www.w3.org/2000/svg">
+<rect width="54" height="42" fill="white"/>
+<rect width="54" height="11" fill="#F9F9FB"/>
+<rect width="54" height="5" fill="#F0F0F4"/>
+<rect x="5" y="2" width="12" height="2" rx="1" fill="#5B5B66"/>
+<rect x="50" y="7" width="2" height="2" rx="1" fill="#5B5B66"/>
+<rect x="2" y="7" width="2" height="2" rx="1" fill="#5B5B66"/>
+<rect x="6" y="7" width="2" height="2" rx="1" fill="#5B5B66"/>
+<rect x="10" y="7" width="38" height="2" rx="1" fill="#5B5B66"/>
+<rect y="11" width="54" height="31" fill="#42414D"/>
+<rect x="4" y="28" width="27" height="2" rx="1" fill="#CFCFD8"/>
+<rect x="4" y="32" width="16" height="2" rx="1" fill="#CFCFD8"/>
+<rect x="4" y="36" width="31" height="2" rx="1" fill="#CFCFD8"/>
+</svg>
diff --git a/browser/components/preferences/web-appearance-light.svg b/browser/components/preferences/web-appearance-light.svg
new file mode 100644
index 0000000000..6d6af7e397
--- /dev/null
+++ b/browser/components/preferences/web-appearance-light.svg
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg viewBox="0 0 54 42" xmlns="http://www.w3.org/2000/svg">
+<rect width="54" height="42" fill="white"/>
+<rect width="54" height="11" fill="#F9F9FB"/>
+<rect width="54" height="5" fill="#F0F0F4"/>
+<rect x="5" y="2" width="12" height="2" rx="1" fill="#5B5B66"/>
+<rect x="50" y="7" width="2" height="2" rx="1" fill="#5B5B66"/>
+<rect x="2" y="7" width="2" height="2" rx="1" fill="#5B5B66"/>
+<rect x="6" y="7" width="2" height="2" rx="1" fill="#5B5B66"/>
+<rect x="10" y="7" width="38" height="2" rx="1" fill="#5B5B66"/>
+<rect y="11" width="54" height="31" fill="white"/>
+<rect x="4" y="28" width="27" height="2" rx="1" fill="#5B5B66"/>
+<rect x="4" y="32" width="16" height="2" rx="1" fill="#5B5B66"/>
+<rect x="4" y="36" width="31" height="2" rx="1" fill="#5B5B66"/>
+</svg>