diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /browser/components/preferences/in-content | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'browser/components/preferences/in-content')
59 files changed, 12003 insertions, 0 deletions
diff --git a/browser/components/preferences/in-content/advanced.js b/browser/components/preferences/in-content/advanced.js new file mode 100644 index 000000000..448a21dae --- /dev/null +++ b/browser/components/preferences/in-content/advanced.js @@ -0,0 +1,770 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Load DownloadUtils module for convertByteUnits +Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); +Components.utils.import("resource://gre/modules/LoadContextInfo.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +var gAdvancedPane = { + _inited: false, + + /** + * Brings the appropriate tab to the front and initializes various bits of UI. + */ + init: function () + { + function setEventListener(aId, aEventType, aCallback) + { + document.getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gAdvancedPane)); + } + + this._inited = true; + var advancedPrefs = document.getElementById("advancedPrefs"); + + var preference = document.getElementById("browser.preferences.advanced.selectedTabIndex"); + if (preference.value !== null) + advancedPrefs.selectedIndex = preference.value; + + if (AppConstants.MOZ_UPDATER) { + let onUnload = function () { + window.removeEventListener("unload", onUnload, false); + Services.prefs.removeObserver("app.update.", this); + }.bind(this); + window.addEventListener("unload", onUnload, false); + Services.prefs.addObserver("app.update.", this, false); + this.updateReadPrefs(); + } + this.updateOfflineApps(); + if (AppConstants.MOZ_CRASHREPORTER) { + this.initSubmitCrashes(); + } + this.initTelemetry(); + if (AppConstants.MOZ_TELEMETRY_REPORTING) { + this.initSubmitHealthReport(); + } + this.updateOnScreenKeyboardVisibility(); + this.updateCacheSizeInputField(); + this.updateActualCacheSize(); + this.updateActualAppCacheSize(); + + setEventListener("layers.acceleration.disabled", "change", + gAdvancedPane.updateHardwareAcceleration); + setEventListener("advancedPrefs", "select", + gAdvancedPane.tabSelectionChanged); + if (AppConstants.MOZ_TELEMETRY_REPORTING) { + setEventListener("submitHealthReportBox", "command", + gAdvancedPane.updateSubmitHealthReport); + } + + setEventListener("connectionSettings", "command", + gAdvancedPane.showConnections); + setEventListener("clearCacheButton", "command", + gAdvancedPane.clearCache); + setEventListener("clearOfflineAppCacheButton", "command", + gAdvancedPane.clearOfflineAppCache); + setEventListener("offlineNotifyExceptions", "command", + gAdvancedPane.showOfflineExceptions); + setEventListener("offlineAppsList", "select", + gAdvancedPane.offlineAppSelected); + let bundlePrefs = document.getElementById("bundlePreferences"); + document.getElementById("offlineAppsList") + .style.height = bundlePrefs.getString("offlineAppsList.height"); + setEventListener("offlineAppsListRemove", "command", + gAdvancedPane.removeOfflineApp); + if (AppConstants.MOZ_UPDATER) { + setEventListener("updateRadioGroup", "command", + gAdvancedPane.updateWritePrefs); + setEventListener("showUpdateHistory", "command", + gAdvancedPane.showUpdates); + } + setEventListener("viewCertificatesButton", "command", + gAdvancedPane.showCertificates); + setEventListener("viewSecurityDevicesButton", "command", + gAdvancedPane.showSecurityDevices); + setEventListener("cacheSize", "change", + gAdvancedPane.updateCacheSizePref); + + if (AppConstants.MOZ_WIDGET_GTK) { + // GTK tabbox' allow the scroll wheel to change the selected tab, + // but we don't want this behavior for the in-content preferences. + let tabsElement = document.getElementById("tabsElement"); + tabsElement.addEventListener("DOMMouseScroll", event => { + event.stopPropagation(); + }, true); + } + }, + + /** + * Stores the identity of the current tab in preferences so that the selected + * tab can be persisted between openings of the preferences window. + */ + tabSelectionChanged: function () + { + if (!this._inited) + return; + var advancedPrefs = document.getElementById("advancedPrefs"); + var preference = document.getElementById("browser.preferences.advanced.selectedTabIndex"); + + // tabSelectionChanged gets called twice due to the selectedIndex being set + // by both the selectedItem and selectedPanel callstacks. This guard is used + // to prevent double-counting in Telemetry. + if (preference.valueFromPreferences != advancedPrefs.selectedIndex) { + Services.telemetry + .getHistogramById("FX_PREFERENCES_CATEGORY_OPENED") + .add(telemetryBucketForCategory("advanced")); + } + + preference.valueFromPreferences = advancedPrefs.selectedIndex; + }, + + // GENERAL TAB + + /* + * Preferences: + * + * 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 + * 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) + * 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 + * layout.spellcheckDefault + * - an integer: + * 0 disables spellchecking + * 1 enables spellchecking, but only for multiline text fields + * 2 enables spellchecking for all text fields + */ + + /** + * 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. + */ + readCheckSpelling: function () + { + var pref = document.getElementById("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: function () + { + var checkbox = document.getElementById("checkSpelling"); + if (checkbox.checked) { + if (this._storedSpellCheck == 2) { + return 2; + } + return 1; + } + return 0; + }, + + /** + * security.OCSP.enabled is an integer value for legacy reasons. + * A value of 1 means OCSP is enabled. Any other value means it is disabled. + */ + readEnableOCSP: function () + { + var preference = document.getElementById("security.OCSP.enabled"); + // This is the case if the preference is the default value. + if (preference.value === undefined) { + return true; + } + return preference.value == 1; + }, + + /** + * See documentation for readEnableOCSP. + */ + writeEnableOCSP: function () + { + var checkbox = document.getElementById("enableOCSP"); + return checkbox.checked ? 1 : 0; + }, + + /** + * When the user toggles the layers.acceleration.disabled pref, + * sync its new value to the gfx.direct2d.disabled pref too. + */ + updateHardwareAcceleration: function() + { + if (AppConstants.platform = "win") { + var fromPref = document.getElementById("layers.acceleration.disabled"); + var toPref = document.getElementById("gfx.direct2d.disabled"); + toPref.value = fromPref.value; + } + }, + + // DATA CHOICES TAB + + /** + * Set up or hide the Learn More links for various data collection options + */ + _setupLearnMoreLink: function (pref, element) { + // set up the Learn More link with the correct URL + let url = Services.prefs.getCharPref(pref); + let el = document.getElementById(element); + + if (url) { + el.setAttribute("href", url); + } else { + el.setAttribute("hidden", "true"); + } + }, + + /** + * + */ + initSubmitCrashes: function () + { + this._setupLearnMoreLink("toolkit.crashreporter.infoURL", + "crashReporterLearnMore"); + }, + + /** + * The preference/checkbox is configured in XUL. + * + * In all cases, set up the Learn More link sanely. + */ + initTelemetry: function () + { + if (AppConstants.MOZ_TELEMETRY_REPORTING) { + this._setupLearnMoreLink("toolkit.telemetry.infoURL", "telemetryLearnMore"); + } + }, + + /** + * Set the status of the telemetry controls based on the input argument. + * @param {Boolean} aEnabled False disables the controls, true enables them. + */ + setTelemetrySectionEnabled: function (aEnabled) + { + if (AppConstants.MOZ_TELEMETRY_REPORTING) { + // If FHR is disabled, additional data sharing should be disabled as well. + let disabled = !aEnabled; + document.getElementById("submitTelemetryBox").disabled = disabled; + if (disabled) { + // If we disable FHR, untick the telemetry checkbox. + Services.prefs.setBoolPref("toolkit.telemetry.enabled", false); + } + document.getElementById("telemetryDataDesc").disabled = disabled; + } + }, + + /** + * Initialize the health report service reference and checkbox. + */ + initSubmitHealthReport: function () { + if (AppConstants.MOZ_TELEMETRY_REPORTING) { + this._setupLearnMoreLink("datareporting.healthreport.infoURL", "FHRLearnMore"); + + let checkbox = document.getElementById("submitHealthReportBox"); + + if (Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED)) { + checkbox.setAttribute("disabled", "true"); + return; + } + + checkbox.checked = Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED); + this.setTelemetrySectionEnabled(checkbox.checked); + } + }, + + /** + * Update the health report preference with state from checkbox. + */ + updateSubmitHealthReport: function () { + if (AppConstants.MOZ_TELEMETRY_REPORTING) { + let checkbox = document.getElementById("submitHealthReportBox"); + Services.prefs.setBoolPref(PREF_UPLOAD_ENABLED, checkbox.checked); + this.setTelemetrySectionEnabled(checkbox.checked); + } + }, + + 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; + } + } + }, + + // NETWORK TAB + + /* + * Preferences: + * + * browser.cache.disk.capacity + * - the size of the browser cache in KB + * - Only used if browser.cache.disk.smart_size.enabled is disabled + */ + + /** + * Displays a dialog in which proxy settings may be changed. + */ + showConnections: function () + { + gSubDialog.open("chrome://browser/content/preferences/connection.xul"); + }, + + // Retrieves the amount of space currently used by disk cache + updateActualCacheSize: function () + { + var actualSizeLabel = document.getElementById("actualDiskCacheSize"); + var prefStrBundle = document.getElementById("bundlePreferences"); + + // Needs to root the observer since cache service keeps only a weak reference. + this.observer = { + onNetworkCacheDiskConsumption: function(consumption) { + var size = DownloadUtils.convertByteUnits(consumption); + // The XBL binding for the string bundle may have been destroyed if + // the page was closed before this callback was executed. + if (!prefStrBundle.getFormattedString) { + return; + } + actualSizeLabel.value = prefStrBundle.getFormattedString("actualDiskCacheSize", size); + }, + + QueryInterface: XPCOMUtils.generateQI([ + Components.interfaces.nsICacheStorageConsumptionObserver, + Components.interfaces.nsISupportsWeakReference + ]) + }; + + actualSizeLabel.value = prefStrBundle.getString("actualDiskCacheSizeCalculated"); + + try { + var cacheService = + Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + cacheService.asyncGetDiskConsumption(this.observer); + } catch (e) {} + }, + + // Retrieves the amount of space currently used by offline cache + updateActualAppCacheSize: function () + { + var visitor = { + onCacheStorageInfo: function (aEntryCount, aConsumption, aCapacity, aDiskDirectory) + { + var actualSizeLabel = document.getElementById("actualAppCacheSize"); + var sizeStrings = DownloadUtils.convertByteUnits(aConsumption); + var prefStrBundle = document.getElementById("bundlePreferences"); + // The XBL binding for the string bundle may have been destroyed if + // the page was closed before this callback was executed. + if (!prefStrBundle.getFormattedString) { + return; + } + var sizeStr = prefStrBundle.getFormattedString("actualAppCacheSize", sizeStrings); + actualSizeLabel.value = sizeStr; + } + }; + + try { + var cacheService = + Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + var storage = cacheService.appCacheStorage(LoadContextInfo.default, null); + storage.asyncVisitStorage(visitor, false); + } catch (e) {} + }, + + updateCacheSizeUI: function (smartSizeEnabled) + { + document.getElementById("useCacheBefore").disabled = smartSizeEnabled; + document.getElementById("cacheSize").disabled = smartSizeEnabled; + document.getElementById("useCacheAfter").disabled = smartSizeEnabled; + }, + + readSmartSizeEnabled: function () + { + // The smart_size.enabled preference element is inverted="true", so its + // value is the opposite of the actual pref value + var disabled = document.getElementById("browser.cache.disk.smart_size.enabled").value; + this.updateCacheSizeUI(!disabled); + }, + + /** + * Converts the cache size from units of KB to units of MB and stores it in + * the textbox element. + */ + updateCacheSizeInputField() + { + let cacheSizeElem = document.getElementById("cacheSize"); + let cachePref = document.getElementById("browser.cache.disk.capacity"); + cacheSizeElem.value = cachePref.value / 1024; + if (cachePref.locked) + cacheSizeElem.disabled = true; + }, + + /** + * Updates the cache size preference once user enters a new value. + * We intentionally do not set preference="browser.cache.disk.capacity" + * onto the textbox directly, as that would update the pref at each keypress + * not only after the final value is entered. + */ + updateCacheSizePref() + { + let cacheSizeElem = document.getElementById("cacheSize"); + let cachePref = document.getElementById("browser.cache.disk.capacity"); + // Converts the cache size as specified in UI (in MB) to KB. + let intValue = parseInt(cacheSizeElem.value, 10); + cachePref.value = isNaN(intValue) ? 0 : intValue * 1024; + }, + + /** + * Clears the cache. + */ + clearCache: function () + { + try { + var cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + cache.clear(); + } catch (ex) {} + this.updateActualCacheSize(); + }, + + /** + * Clears the application cache. + */ + clearOfflineAppCache: function () + { + Components.utils.import("resource:///modules/offlineAppCache.jsm"); + OfflineAppCacheHelper.clear(); + + this.updateActualAppCacheSize(); + this.updateOfflineApps(); + }, + + readOfflineNotify: function() + { + var pref = document.getElementById("browser.offline-apps.notify"); + var button = document.getElementById("offlineNotifyExceptions"); + button.disabled = !pref.value; + return pref.value; + }, + + showOfflineExceptions: function() + { + var bundlePreferences = document.getElementById("bundlePreferences"); + var params = { blockVisible : false, + sessionVisible : false, + allowVisible : false, + prefilledHost : "", + permissionType : "offline-app", + manageCapability : Components.interfaces.nsIPermissionManager.DENY_ACTION, + windowTitle : bundlePreferences.getString("offlinepermissionstitle"), + introText : bundlePreferences.getString("offlinepermissionstext") }; + gSubDialog.open("chrome://browser/content/preferences/permissions.xul", + null, params); + }, + + // XXX: duplicated in browser.js + _getOfflineAppUsage(perm, groups) { + let cacheService = Cc["@mozilla.org/network/application-cache-service;1"]. + getService(Ci.nsIApplicationCacheService); + if (!groups) { + try { + groups = cacheService.getGroups(); + } catch (ex) { + return 0; + } + } + + let usage = 0; + for (let group of groups) { + let uri = Services.io.newURI(group, null, null); + if (perm.matchesURI(uri, true)) { + let cache = cacheService.getActiveCache(group); + usage += cache.usage; + } + } + + return usage; + }, + + /** + * Updates the list of offline applications + */ + updateOfflineApps: function () + { + var pm = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); + + var list = document.getElementById("offlineAppsList"); + while (list.firstChild) { + list.removeChild(list.firstChild); + } + + var groups; + try { + var cacheService = Components.classes["@mozilla.org/network/application-cache-service;1"]. + getService(Components.interfaces.nsIApplicationCacheService); + groups = cacheService.getGroups(); + } catch (e) { + return; + } + + var bundle = document.getElementById("bundlePreferences"); + + var enumerator = pm.enumerator; + while (enumerator.hasMoreElements()) { + var perm = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission); + if (perm.type == "offline-app" && + perm.capability != Components.interfaces.nsIPermissionManager.DEFAULT_ACTION && + perm.capability != Components.interfaces.nsIPermissionManager.DENY_ACTION) { + var row = document.createElement("listitem"); + row.id = ""; + row.className = "offlineapp"; + row.setAttribute("origin", perm.principal.origin); + var converted = DownloadUtils. + convertByteUnits(this._getOfflineAppUsage(perm, groups)); + row.setAttribute("usage", + bundle.getFormattedString("offlineAppUsage", + converted)); + list.appendChild(row); + } + } + }, + + offlineAppSelected: function() + { + var removeButton = document.getElementById("offlineAppsListRemove"); + var list = document.getElementById("offlineAppsList"); + if (list.selectedItem) { + removeButton.setAttribute("disabled", "false"); + } else { + removeButton.setAttribute("disabled", "true"); + } + }, + + removeOfflineApp: function() + { + var list = document.getElementById("offlineAppsList"); + var item = list.selectedItem; + var origin = item.getAttribute("origin"); + var principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin); + + var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + var flags = prompts.BUTTON_TITLE_IS_STRING * prompts.BUTTON_POS_0 + + prompts.BUTTON_TITLE_CANCEL * prompts.BUTTON_POS_1; + + var bundle = document.getElementById("bundlePreferences"); + var title = bundle.getString("offlineAppRemoveTitle"); + var prompt = bundle.getFormattedString("offlineAppRemovePrompt", [principal.URI.prePath]); + var confirm = bundle.getString("offlineAppRemoveConfirm"); + var result = prompts.confirmEx(window, title, prompt, flags, confirm, + null, null, null, {}); + if (result != 0) + return; + + // get the permission + var pm = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); + var perm = pm.getPermissionObject(principal, "offline-app", true); + if (perm) { + // clear offline cache entries + try { + var cacheService = Components.classes["@mozilla.org/network/application-cache-service;1"]. + getService(Components.interfaces.nsIApplicationCacheService); + var groups = cacheService.getGroups(); + for (var i = 0; i < groups.length; i++) { + var uri = Services.io.newURI(groups[i], null, null); + if (perm.matchesURI(uri, true)) { + var cache = cacheService.getActiveCache(groups[i]); + cache.discard(); + } + } + } catch (e) {} + + pm.removePermission(perm); + } + list.removeChild(item); + gAdvancedPane.offlineAppSelected(); + this.updateActualAppCacheSize(); + }, + + // UPDATE TAB + + /* + * Preferences: + * + * app.update.enabled + * - true if updates to the application are enabled, false otherwise + * app.update.auto + * - true if updates should be automatically downloaded and installed and + * false if the user should be asked what he wants to do when an update is + * available + * extensions.update.enabled + * - true if updates to extensions and themes are enabled, false otherwise + * browser.search.update + * - true if updates to search engines are enabled, false otherwise + */ + + /** + * Selects the item of the radiogroup based on the pref values and locked + * states. + * + * UI state matrix for update preference conditions + * + * UI Components: Preferences + * Radiogroup i = app.update.enabled + * ii = app.update.auto + * + * Disabled states: + * Element pref value locked disabled + * radiogroup i t/f f false + * i t/f *t* *true* + * ii t/f f false + * ii t/f *t* *true* + */ + updateReadPrefs: function () + { + if (AppConstants.MOZ_UPDATER) { + var enabledPref = document.getElementById("app.update.enabled"); + var autoPref = document.getElementById("app.update.auto"); + var radiogroup = document.getElementById("updateRadioGroup"); + + if (!enabledPref.value) // Don't care for autoPref.value in this case. + radiogroup.value="manual"; // 3. Never check for updates. + else if (autoPref.value) // enabledPref.value && autoPref.value + radiogroup.value="auto"; // 1. Automatically install updates + else // enabledPref.value && !autoPref.value + radiogroup.value="checkOnly"; // 2. Check, but let me choose + + var canCheck = Components.classes["@mozilla.org/updates/update-service;1"]. + getService(Components.interfaces.nsIApplicationUpdateService). + canCheckForUpdates; + // canCheck is false if the enabledPref is false and locked, + // or the binary platform or OS version is not known. + // A locked pref is sufficient to disable the radiogroup. + radiogroup.disabled = !canCheck || enabledPref.locked || autoPref.locked; + + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + // Check to see if the maintenance service is installed. + // If it is don't show the preference at all. + var installed; + try { + var wrk = Components.classes["@mozilla.org/windows-registry-key;1"] + .createInstance(Components.interfaces.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; + } + } + } + }, + + /** + * Sets the pref values based on the selected item of the radiogroup. + */ + updateWritePrefs: function () + { + if (AppConstants.MOZ_UPDATER) { + var enabledPref = document.getElementById("app.update.enabled"); + var autoPref = document.getElementById("app.update.auto"); + var radiogroup = document.getElementById("updateRadioGroup"); + switch (radiogroup.value) { + case "auto": // 1. Automatically install updates for Desktop only + enabledPref.value = true; + autoPref.value = true; + break; + case "checkOnly": // 2. Check, but let me choose + enabledPref.value = true; + autoPref.value = false; + break; + case "manual": // 3. Never check for updates. + enabledPref.value = false; + autoPref.value = false; + } + } + }, + + /** + * Displays the history of installed updates. + */ + showUpdates: function () + { + gSubDialog.open("chrome://mozapps/content/update/history.xul"); + }, + + // ENCRYPTION TAB + + /* + * Preferences: + * + * 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 + */ + + /** + * Displays the user's certificates and associated options. + */ + showCertificates: function () + { + gSubDialog.open("chrome://pippki/content/certManager.xul"); + }, + + /** + * Displays a dialog from which the user can manage his security devices. + */ + showSecurityDevices: function () + { + gSubDialog.open("chrome://pippki/content/device_manager.xul"); + }, + + observe: function (aSubject, aTopic, aData) { + if (AppConstants.MOZ_UPDATER) { + switch (aTopic) { + case "nsPref:changed": + this.updateReadPrefs(); + break; + } + } + }, +}; diff --git a/browser/components/preferences/in-content/advanced.xul b/browser/components/preferences/in-content/advanced.xul new file mode 100644 index 000000000..facaaeaa9 --- /dev/null +++ b/browser/components/preferences/in-content/advanced.xul @@ -0,0 +1,421 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<!-- Advanced panel --> + +<script type="application/javascript" + src="chrome://browser/content/preferences/in-content/advanced.js"/> + +<preferences id="advancedPreferences" hidden="true" data-category="paneAdvanced"> + <preference id="browser.preferences.advanced.selectedTabIndex" + name="browser.preferences.advanced.selectedTabIndex" + type="int"/> + + <!-- General tab --> + <preference id="accessibility.browsewithcaret" + name="accessibility.browsewithcaret" + type="bool"/> + <preference id="accessibility.typeaheadfind" + name="accessibility.typeaheadfind" + type="bool"/> + <preference id="accessibility.blockautorefresh" + name="accessibility.blockautorefresh" + type="bool"/> +#ifdef XP_WIN + <preference id="ui.osk.enabled" + name="ui.osk.enabled" + type="bool"/> +#endif + + <preference id="general.autoScroll" + name="general.autoScroll" + type="bool"/> + <preference id="general.smoothScroll" + name="general.smoothScroll" + type="bool"/> + <preference id="layers.acceleration.disabled" + name="layers.acceleration.disabled" + type="bool" + inverted="true"/> +#ifdef XP_WIN + <preference id="gfx.direct2d.disabled" + name="gfx.direct2d.disabled" + type="bool" + inverted="true"/> +#endif + <preference id="layout.spellcheckDefault" + name="layout.spellcheckDefault" + type="int"/> + +#ifdef MOZ_TELEMETRY_REPORTING + <preference id="toolkit.telemetry.enabled" + name="toolkit.telemetry.enabled" + type="bool"/> +#endif + + <!-- Data Choices tab --> +#ifdef MOZ_CRASHREPORTER + <preference id="browser.crashReports.unsubmittedCheck.autoSubmit2" + name="browser.crashReports.unsubmittedCheck.autoSubmit2" + type="bool"/> +#endif + + <!-- Network tab --> + <preference id="browser.cache.disk.capacity" + name="browser.cache.disk.capacity" + type="int"/> + <preference id="browser.offline-apps.notify" + name="browser.offline-apps.notify" + type="bool"/> + + <preference id="browser.cache.disk.smart_size.enabled" + name="browser.cache.disk.smart_size.enabled" + inverted="true" + type="bool"/> + + <!-- Update tab --> +#ifdef MOZ_UPDATER + <preference id="app.update.enabled" + name="app.update.enabled" + type="bool"/> + <preference id="app.update.auto" + name="app.update.auto" + type="bool"/> + + <preference id="app.update.disable_button.showUpdateHistory" + name="app.update.disable_button.showUpdateHistory" + type="bool"/> + +#ifdef MOZ_MAINTENANCE_SERVICE + <preference id="app.update.service.enabled" + name="app.update.service.enabled" + type="bool"/> +#endif +#endif + + <preference id="browser.search.update" + name="browser.search.update" + type="bool"/> + + <!-- Certificates tab --> + <preference id="security.default_personal_cert" + name="security.default_personal_cert" + type="string"/> + + <preference id="security.disable_button.openCertManager" + name="security.disable_button.openCertManager" + type="bool"/> + + <preference id="security.disable_button.openDeviceManager" + name="security.disable_button.openDeviceManager" + type="bool"/> + + <preference id="security.OCSP.enabled" + name="security.OCSP.enabled" + type="int"/> +</preferences> + +#ifdef HAVE_SHELL_SERVICE + <stringbundle id="bundleShell" src="chrome://browser/locale/shellservice.properties"/> + <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/> +#endif + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + +<hbox id="header-advanced" + class="header" + hidden="true" + data-category="paneAdvanced"> + <label class="header-name" flex="1">&paneAdvanced.title;</label> + <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a> +</hbox> + +<tabbox id="advancedPrefs" + handleCtrlTab="false" + handleCtrlPageUpDown="false" + flex="1" + data-category="paneAdvanced" + hidden="true"> + + <tabs id="tabsElement"> + <tab id="generalTab" label="&generalTab.label;"/> +#ifdef MOZ_DATA_REPORTING + <tab id="dataChoicesTab" label="&dataChoicesTab.label;"/> +#endif + <tab id="networkTab" label="&networkTab.label;"/> + <tab id="updateTab" label="&updateTab.label;"/> + <tab id="encryptionTab" label="&certificateTab.label;"/> + </tabs> + + <tabpanels flex="1"> + + <!-- General --> + <tabpanel id="generalPanel" orient="vertical"> + <!-- Accessibility --> + <groupbox id="accessibilityGroup" align="start"> + <caption><label>&accessibility.label;</label></caption> + +#ifdef XP_WIN + <checkbox id="useOnScreenKeyboard" + hidden="true" + label="&useOnScreenKeyboard.label;" + accesskey="&useOnScreenKeyboard.accesskey;" + preference="ui.osk.enabled"/> +#endif + <checkbox id="useCursorNavigation" + label="&useCursorNavigation.label;" + accesskey="&useCursorNavigation.accesskey;" + preference="accessibility.browsewithcaret"/> + <checkbox id="searchStartTyping" + label="&searchStartTyping.label;" + accesskey="&searchStartTyping.accesskey;" + preference="accessibility.typeaheadfind"/> + <checkbox id="blockAutoRefresh" + label="&blockAutoRefresh.label;" + accesskey="&blockAutoRefresh.accesskey;" + preference="accessibility.blockautorefresh"/> + </groupbox> + <!-- Browsing --> + <groupbox id="browsingGroup" align="start"> + <caption><label>&browsing.label;</label></caption> + + <checkbox id="useAutoScroll" + label="&useAutoScroll.label;" + accesskey="&useAutoScroll.accesskey;" + preference="general.autoScroll"/> + <checkbox id="useSmoothScrolling" + label="&useSmoothScrolling.label;" + accesskey="&useSmoothScrolling.accesskey;" + preference="general.smoothScroll"/> + <checkbox id="allowHWAccel" + label="&allowHWAccel.label;" + accesskey="&allowHWAccel.accesskey;" + preference="layers.acceleration.disabled"/> + <checkbox id="checkSpelling" + label="&checkSpelling.label;" + accesskey="&checkSpelling.accesskey;" + onsyncfrompreference="return gAdvancedPane.readCheckSpelling();" + onsynctopreference="return gAdvancedPane.writeCheckSpelling();" + preference="layout.spellcheckDefault"/> + </groupbox> + </tabpanel> +#ifdef MOZ_DATA_REPORTING + <!-- Data Choices --> + <tabpanel id="dataChoicesPanel" orient="vertical"> +#ifdef MOZ_TELEMETRY_REPORTING + <groupbox> + <caption> + <checkbox id="submitHealthReportBox" label="&enableHealthReport.label;" + accesskey="&enableHealthReport.accesskey;"/> + </caption> + <vbox> + <hbox class="indent"> + <label flex="1">&healthReportDesc.label;</label> + <spacer flex="10"/> + <label id="FHRLearnMore" + class="text-link">&healthReportLearnMore.label;</label> + </hbox> + <hbox class="indent"> + <groupbox flex="1"> + <caption> + <checkbox id="submitTelemetryBox" preference="toolkit.telemetry.enabled" + label="&enableTelemetryData.label;" + accesskey="&enableTelemetryData.accesskey;"/> + </caption> + <hbox class="indent"> + <label id="telemetryDataDesc" flex="1">&telemetryDesc.label;</label> + <spacer flex="10"/> + <label id="telemetryLearnMore" + class="text-link">&telemetryLearnMore.label;</label> + </hbox> + </groupbox> + </hbox> + </vbox> + </groupbox> +#endif +#ifdef MOZ_CRASHREPORTER + <groupbox> + <caption> + <checkbox id="automaticallySubmitCrashesBox" + preference="browser.crashReports.unsubmittedCheck.autoSubmit2" + label="&alwaysSubmitCrashReports.label;" + accesskey="&alwaysSubmitCrashReports.accesskey;"/> + </caption> + <hbox class="indent"> + <label flex="1">&crashReporterDesc2.label;</label> + <spacer flex="10"/> + <label id="crashReporterLearnMore" + class="text-link">&crashReporterLearnMore.label;</label> + </hbox> + </groupbox> +#endif + </tabpanel> +#endif + + <!-- Network --> + <tabpanel id="networkPanel" orient="vertical"> + + <!-- Connection --> + <groupbox id="connectionGroup"> + <caption><label>&connection.label;</label></caption> + + <hbox align="center"> + <description flex="1" control="connectionSettings">&connectionDesc.label;</description> + <button id="connectionSettings" icon="network" label="&connectionSettings.label;" + accesskey="&connectionSettings.accesskey;"/> + </hbox> + </groupbox> + + <!-- Cache --> + <groupbox id="cacheGroup"> + <caption><label>&httpCache.label;</label></caption> + + <hbox align="center"> + <label id="actualDiskCacheSize" flex="1"/> + <button id="clearCacheButton" icon="clear" + label="&clearCacheNow.label;" accesskey="&clearCacheNow.accesskey;"/> + </hbox> + <hbox> + <checkbox preference="browser.cache.disk.smart_size.enabled" + id="allowSmartSize" + onsyncfrompreference="return gAdvancedPane.readSmartSizeEnabled();" + label="&overrideSmartCacheSize.label;" + accesskey="&overrideSmartCacheSize.accesskey;"/> + </hbox> + <hbox align="center" class="indent"> + <label id="useCacheBefore" control="cacheSize" + accesskey="&limitCacheSizeBefore.accesskey;"> + &limitCacheSizeBefore.label; + </label> + <textbox id="cacheSize" type="number" size="4" max="1024" + aria-labelledby="useCacheBefore cacheSize useCacheAfter"/> + <label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label> + </hbox> + </groupbox> + + <!-- Offline apps --> + <groupbox id="offlineGroup"> + <caption><label>&offlineStorage2.label;</label></caption> + + <hbox align="center"> + <label id="actualAppCacheSize" flex="1"/> + <button id="clearOfflineAppCacheButton" icon="clear" + label="&clearOfflineAppCacheNow.label;" accesskey="&clearOfflineAppCacheNow.accesskey;"/> + </hbox> + <hbox align="center"> + <checkbox id="offlineNotify" + label="&offlineNotify.label;" accesskey="&offlineNotify.accesskey;" + preference="browser.offline-apps.notify" + onsyncfrompreference="return gAdvancedPane.readOfflineNotify();"/> + <spacer flex="1"/> + <button id="offlineNotifyExceptions" + label="&offlineNotifyExceptions.label;" + accesskey="&offlineNotifyExceptions.accesskey;"/> + </hbox> + <hbox> + <vbox flex="1"> + <label id="offlineAppsListLabel">&offlineAppsList2.label;</label> + <listbox id="offlineAppsList" + flex="1" + aria-labelledby="offlineAppsListLabel"> + </listbox> + </vbox> + <vbox pack="end"> + <button id="offlineAppsListRemove" + disabled="true" + label="&offlineAppsListRemove.label;" + accesskey="&offlineAppsListRemove.accesskey;"/> + </vbox> + </hbox> + </groupbox> + </tabpanel> + + <!-- Update --> + <tabpanel id="updatePanel" orient="vertical"> +#ifdef MOZ_UPDATER + <groupbox id="updateApp" align="start"> + <caption><label>&updateApp.label;</label></caption> + <radiogroup id="updateRadioGroup" align="start"> + <radio id="autoDesktop" + value="auto" + label="&updateAuto1.label;" + accesskey="&updateAuto1.accesskey;"/> + <radio value="checkOnly" + label="&updateCheck.label;" + accesskey="&updateCheck.accesskey;"/> + <radio value="manual" + label="&updateManual.label;" + accesskey="&updateManual.accesskey;"/> + </radiogroup> + <separator class="thin"/> + <hbox> + <button id="showUpdateHistory" + label="&updateHistory.label;" + accesskey="&updateHistory.accesskey;" + preference="app.update.disable_button.showUpdateHistory"/> + </hbox> + +#ifdef MOZ_MAINTENANCE_SERVICE + <checkbox id="useService" + label="&useService.label;" + accesskey="&useService.accesskey;" + preference="app.update.service.enabled"/> +#endif + </groupbox> +#endif + <groupbox id="updateOthers" align="start"> + <caption><label>&updateOthers.label;</label></caption> + <checkbox id="enableSearchUpdate" + label="&enableSearchUpdate.label;" + accesskey="&enableSearchUpdate.accesskey;" + preference="browser.search.update"/> + </groupbox> + </tabpanel> + + <!-- Certificates --> + <tabpanel id="encryptionPanel" orient="vertical"> + <groupbox id="certSelection" align="start"> + <caption><label>&certSelection.label;</label></caption> + <description id="CertSelectionDesc" control="certSelection">&certSelection.description;</description> + + <!-- + The values on these radio buttons may look like l12y issues, but + they're not - this preference uses *those strings* as its values. + I KID YOU NOT. + --> + <radiogroup id="certSelection" + preftype="string" + preference="security.default_personal_cert" + aria-labelledby="CertSelectionDesc"> + <radio label="&certs.auto;" + accesskey="&certs.auto.accesskey;" + value="Select Automatically"/> + <radio label="&certs.ask;" + accesskey="&certs.ask.accesskey;" + value="Ask Every Time"/> + </radiogroup> + </groupbox> + <separator/> + <checkbox id="enableOCSP" + label="&enableOCSP.label;" + accesskey="&enableOCSP.accesskey;" + onsyncfrompreference="return gAdvancedPane.readEnableOCSP();" + onsynctopreference="return gAdvancedPane.writeEnableOCSP();" + preference="security.OCSP.enabled"/> + <separator/> + <hbox> + <button id="viewCertificatesButton" + flex="1" + label="&viewCerts.label;" + accesskey="&viewCerts.accesskey;" + preference="security.disable_button.openCertManager"/> + <button id="viewSecurityDevicesButton" + flex="1" + label="&viewSecurityDevices.label;" + accesskey="&viewSecurityDevices.accesskey;" + preference="security.disable_button.openDeviceManager"/> + <hbox flex="10"/> + </hbox> + </tabpanel> + </tabpanels> +</tabbox> diff --git a/browser/components/preferences/in-content/applications.js b/browser/components/preferences/in-content/applications.js new file mode 100644 index 000000000..6f2989657 --- /dev/null +++ b/browser/components/preferences/in-content/applications.js @@ -0,0 +1,1900 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +// Constants & Enumeration Values + +Components.utils.import('resource://gre/modules/Services.jsm'); +Components.utils.import('resource://gre/modules/AppConstants.jsm'); +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_PDF = "application/pdf"; + +const PREF_PDFJS_DISABLED = "pdfjs.disabled"; +const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged"; + +const PREF_DISABLED_PLUGIN_TYPES = "plugin.disable_full_page_plugin_for_types"; + +// Preferences that affect which entries to show in the list. +const PREF_SHOW_PLUGINS_IN_LIST = "browser.download.show_plugins_in_list"; +const PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS = + "browser.download.hide_plugins_without_extensions"; + +/* + * Preferences where we store handling information about the feed type. + * + * browser.feeds.handler + * - "bookmarks", "reader" (clarified further using the .default preference), + * or "ask" -- indicates the default handler being used to process feeds; + * "bookmarks" is obsolete; to specify that the handler is bookmarks, + * set browser.feeds.handler.default to "bookmarks"; + * + * browser.feeds.handler.default + * - "bookmarks", "client" or "web" -- indicates the chosen feed reader used + * to display feeds, either transiently (i.e., when the "use as default" + * checkbox is unchecked, corresponds to when browser.feeds.handler=="ask") + * or more permanently (i.e., the item displayed in the dropdown in Feeds + * preferences) + * + * browser.feeds.handler.webservice + * - the URL of the currently selected web service used to read feeds + * + * browser.feeds.handlers.application + * - nsILocalFile, stores the current client-side feed reading app if one has + * been chosen + */ +const PREF_FEED_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_FEED_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_FEED_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_FEED_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_FEED_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_FEED_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_FEED_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_FEED_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_FEED_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_FEED_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_FEED_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_FEED_SELECTED_READER = "browser.audioFeeds.handler.default"; + +// The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify +// the actions the application can take with content of various types. +// But since nsIHandlerInfo doesn't support plugins, there's no value +// identifying the "use plugin" action, so we use this constant instead. +const kActionUsePlugin = 5; + +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", "plugin" or "feed". 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"; + +// 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; +} + +/** + * An enumeration of items in a JS array. + * + * FIXME: use ArrayConverter once it lands (bug 380839). + * + * @constructor + */ +function ArrayEnumerator(aItems) { + this._index = 0; + this._contents = aItems; +} + +ArrayEnumerator.prototype = { + _index: 0, + + hasMoreElements: function() { + return this._index < this._contents.length; + }, + + getNext: function() { + return this._contents[this._index++]; + } +}; + +function isFeedType(t) { + return t == TYPE_MAYBE_FEED || t == TYPE_MAYBE_VIDEO_FEED || t == TYPE_MAYBE_AUDIO_FEED; +} + +// HandlerInfoWrapper + +/** + * 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 plugins and the handler service. + * + * We don't implement all the original nsIHandlerInfo functionality, + * just the stuff that the prefpane needs. + * + * In theory, all of the custom functionality in this wrapper should get + * pushed down into nsIHandlerInfo eventually. + */ +function HandlerInfoWrapper(aType, aHandlerInfo) { + this._type = aType; + this.wrappedHandlerInfo = aHandlerInfo; +} + +HandlerInfoWrapper.prototype = { + // The wrapped nsIHandlerInfo object. In general, this object is private, + // but there are a couple cases where callers access it directly for things + // we haven't (yet?) implemented, so we make it a public property. + wrappedHandlerInfo: null, + + + // Convenience Utils + + _handlerSvc: Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService), + + _prefSvc: Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch), + + _categoryMgr: Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager), + + element: function(aID) { + return document.getElementById(aID); + }, + + + // nsIHandlerInfo + + // The MIME type or protocol scheme. + _type: null, + get type() { + return this._type; + }, + + get description() { + if (this.wrappedHandlerInfo.description) + return this.wrappedHandlerInfo.description; + + if (this.primaryExtension) { + var extension = this.primaryExtension.toUpperCase(); + return this.element("bundlePreferences").getFormattedString("fileEnding", + [extension]); + } + + return this.type; + }, + + 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: function(aNewHandler) { + var possibleApps = this.possibleApplicationHandlers.enumerate(); + while (possibleApps.hasMoreElements()) { + if (possibleApps.getNext().equals(aNewHandler)) + return; + } + this.possibleApplicationHandlers.appendElement(aNewHandler, false); + }, + + removePossibleApplicationHandler: function(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 we have an enabled plugin, then the action is to use that plugin. + if (this.pluginName && !this.isDisabledPluginType) + return kActionUsePlugin; + + // 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 && + !gApplicationsPane.isValidHandlerApp(this.preferredApplicationHandler)) { + if (this.wrappedHandlerInfo.hasDefaultHandler) + return Ci.nsIHandlerInfo.useSystemDefault; + return Ci.nsIHandlerInfo.saveToDisk; + } + + return this.wrappedHandlerInfo.preferredAction; + }, + + set preferredAction(aNewValue) { + // If the action is to use the plugin, + // we must set the preferred action to "save to disk". + // But only if it's not currently the preferred action. + if ((aNewValue == kActionUsePlugin) && + (this.preferredAction != Ci.nsIHandlerInfo.saveToDisk)) { + aNewValue = Ci.nsIHandlerInfo.saveToDisk; + } + + // We don't modify the preferred action if the new action is to use a plugin + // because handler info objects don't understand our custom "use plugin" + // value. Also, leaving it untouched means that we can automatically revert + // to the old setting if the user ever removes the plugin. + + if (aNewValue != kActionUsePlugin) + this.wrappedHandlerInfo.preferredAction = aNewValue; + }, + + get alwaysAskBeforeHandling() { + // If this type is handled only by a plugin, we can't trust the value + // in the handler info object, since it'll be a default based on the absence + // of any user configuration, and the default in that case is to always ask, + // even though we never ask for content handled by a plugin, so special case + // plugin-handled types by returning false here. + if (this.pluginName && this.handledOnlyByPlugin) + return false; + + // 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; + }, + + + // nsIMIMEInfo + + // The primary file extension associated with this type, if any. + // + // XXX Plugin objects contain an array of MimeType objects with "suffixes" + // properties; if this object has an associated plugin, shouldn't we check + // those properties for an extension? + get primaryExtension() { + try { + if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo.primaryExtension) + return this.wrappedHandlerInfo.primaryExtension + } catch (ex) {} + + return null; + }, + + + // Plugin Handling + + // A plugin that can handle this type, if any. + // + // Note: just because we have one doesn't mean it *will* handle the type. + // That depends on whether or not the type is in the list of types for which + // plugin handling is disabled. + plugin: null, + + // Whether or not this type is only handled by a plugin or is also handled + // by some user-configured action as specified in the handler info object. + // + // Note: we can't just check if there's a handler info object for this type, + // because OS and user configuration is mixed up in the handler info object, + // so we always need to retrieve it for the OS info and can't tell whether + // it represents only OS-default information or user-configured information. + // + // FIXME: once handler info records are broken up into OS-provided records + // and user-configured records, stop using this boolean flag and simply + // check for the presence of a user-configured record to determine whether + // or not this type is only handled by a plugin. Filed as bug 395142. + handledOnlyByPlugin: undefined, + + get isDisabledPluginType() { + return this._getDisabledPluginTypes().indexOf(this.type) != -1; + }, + + _getDisabledPluginTypes: function() { + var types = ""; + + if (this._prefSvc.prefHasUserValue(PREF_DISABLED_PLUGIN_TYPES)) + types = this._prefSvc.getCharPref(PREF_DISABLED_PLUGIN_TYPES); + + // Only split if the string isn't empty so we don't end up with an array + // containing a single empty string. + if (types != "") + return types.split(","); + + return []; + }, + + disablePluginType: function() { + var disabledPluginTypes = this._getDisabledPluginTypes(); + + if (disabledPluginTypes.indexOf(this.type) == -1) + disabledPluginTypes.push(this.type); + + this._prefSvc.setCharPref(PREF_DISABLED_PLUGIN_TYPES, + disabledPluginTypes.join(",")); + + // Update the category manager so existing browser windows update. + this._categoryMgr.deleteCategoryEntry("Gecko-Content-Viewers", + this.type, + false); + }, + + enablePluginType: function() { + var disabledPluginTypes = this._getDisabledPluginTypes(); + + var type = this.type; + disabledPluginTypes = disabledPluginTypes.filter(v => v != type); + + this._prefSvc.setCharPref(PREF_DISABLED_PLUGIN_TYPES, + disabledPluginTypes.join(",")); + + // Update the category manager so existing browser windows update. + this._categoryMgr. + addCategoryEntry("Gecko-Content-Viewers", + this.type, + "@mozilla.org/content/plugin/document-loader-factory;1", + false, + true); + }, + + + // Storage + + store: function() { + this._handlerSvc.store(this.wrappedHandlerInfo); + }, + + + // Icons + + get smallIcon() { + return this._getIcon(16); + }, + + _getIcon: function(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; + } + +}; + + +// Feed Handler Info + +/** + * This object implements nsIHandlerInfo for the feed types. It's a separate + * object because we currently store handling information for the feed type + * in a set of preferences rather than the nsIHandlerService-managed datastore. + * + * This object inherits from HandlerInfoWrapper in order to get functionality + * that isn't special to the feed type. + * + * XXX Should we inherit from HandlerInfoWrapper? After all, we override + * most of that wrapper's properties and methods, and we have to dance around + * the fact that the wrapper expects to have a wrappedHandlerInfo, which we + * don't provide. + */ + +function FeedHandlerInfo(aMIMEType) { + HandlerInfoWrapper.call(this, aMIMEType, null); +} + +FeedHandlerInfo.prototype = { + __proto__: HandlerInfoWrapper.prototype, + + // Convenience Utils + + _converterSvc: + Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService), + + _shellSvc: AppConstants.HAVE_SHELL_SERVICE ? getShellService() : null, + + // nsIHandlerInfo + + get description() { + return this.element("bundlePreferences").getString(this._appPrefLabel); + }, + + get preferredApplicationHandler() { + switch (this.element(this._prefSelectedReader).value) { + case "client": + var file = this.element(this._prefSelectedApp).value; + if (file) + return getLocalHandlerApp(file); + + return null; + + case "web": + var uri = this.element(this._prefSelectedWeb).value; + if (!uri) + return null; + return this._converterSvc.getWebContentHandlerByURI(this.type, uri); + + case "bookmarks": + default: + // When the pref is set to bookmarks, we handle feeds internally, + // we don't forward them to a local or web handler app, so there is + // no preferred handler. + return null; + } + }, + + set preferredApplicationHandler(aNewValue) { + if (aNewValue instanceof Ci.nsILocalHandlerApp) { + this.element(this._prefSelectedApp).value = aNewValue.executable; + this.element(this._prefSelectedReader).value = "client"; + } + else if (aNewValue instanceof Ci.nsIWebContentHandlerInfo) { + this.element(this._prefSelectedWeb).value = aNewValue.uri; + this.element(this._prefSelectedReader).value = "web"; + // Make the web handler be the new "auto handler" for feeds. + // Note: we don't have to unregister the auto handler when the user picks + // a non-web handler (local app, Live Bookmarks, etc.) because the service + // only uses the "auto handler" when the selected reader is a web handler. + // We also don't have to unregister it when the user turns on "always ask" + // (i.e. preview in browser), since that also overrides the auto handler. + this._converterSvc.setAutoHandler(this.type, aNewValue); + } + }, + + _possibleApplicationHandlers: null, + + get possibleApplicationHandlers() { + if (this._possibleApplicationHandlers) + return this._possibleApplicationHandlers; + + // A minimal implementation of nsIMutableArray. It only supports the two + // methods its callers invoke, namely appendElement and nsIArray::enumerate. + this._possibleApplicationHandlers = { + _inner: [], + _removed: [], + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIMutableArray) || + aIID.equals(Ci.nsIArray) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + get length() { + return this._inner.length; + }, + + enumerate: function() { + return new ArrayEnumerator(this._inner); + }, + + appendElement: function(aHandlerApp, aWeak) { + this._inner.push(aHandlerApp); + }, + + removeElementAt: function(aIndex) { + this._removed.push(this._inner[aIndex]); + this._inner.splice(aIndex, 1); + }, + + queryElementAt: function(aIndex, aInterface) { + return this._inner[aIndex].QueryInterface(aInterface); + } + }; + + // Add the selected local app if it's different from the OS default handler. + // Unlike for other types, we can store only one local app at a time for the + // feed type, since we store it in a preference that historically stores + // only a single path. But we display all the local apps the user chooses + // while the prefpane is open, only dropping the list when the user closes + // the prefpane, for maximum usability and consistency with other types. + var preferredAppFile = this.element(this._prefSelectedApp).value; + if (preferredAppFile) { + let preferredApp = getLocalHandlerApp(preferredAppFile); + let defaultApp = this._defaultApplicationHandler; + if (!defaultApp || !defaultApp.equals(preferredApp)) + this._possibleApplicationHandlers.appendElement(preferredApp, false); + } + + // Add the registered web handlers. There can be any number of these. + var webHandlers = this._converterSvc.getContentHandlers(this.type); + for (let webHandler of webHandlers) + this._possibleApplicationHandlers.appendElement(webHandler, false); + + return this._possibleApplicationHandlers; + }, + + __defaultApplicationHandler: undefined, + get _defaultApplicationHandler() { + if (typeof this.__defaultApplicationHandler != "undefined") + return this.__defaultApplicationHandler; + + var defaultFeedReader = null; + if (AppConstants.HAVE_SHELL_SERVICE) { + try { + defaultFeedReader = this._shellSvc.defaultFeedReader; + } + catch (ex) { + // no default reader or _shellSvc is null + } + } + + if (defaultFeedReader) { + let handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsIHandlerApp); + handlerApp.name = getFileDisplayName(defaultFeedReader); + handlerApp.QueryInterface(Ci.nsILocalHandlerApp); + handlerApp.executable = defaultFeedReader; + + this.__defaultApplicationHandler = handlerApp; + } + else { + this.__defaultApplicationHandler = null; + } + + return this.__defaultApplicationHandler; + }, + + get hasDefaultHandler() { + if (AppConstants.HAVE_SHELL_SERVICE) { + try { + if (this._shellSvc.defaultFeedReader) + return true; + } + catch (ex) { + // no default reader or _shellSvc is null + } + } + + return false; + }, + + get defaultDescription() { + if (this.hasDefaultHandler) + return this._defaultApplicationHandler.name; + + // Should we instead return null? + return ""; + }, + + // What to do with content of this type. + get preferredAction() { + switch (this.element(this._prefSelectedAction).value) { + + case "bookmarks": + return Ci.nsIHandlerInfo.handleInternally; + + case "reader": { + let preferredApp = this.preferredApplicationHandler; + let defaultApp = this._defaultApplicationHandler; + + // If we have a valid preferred app, return useSystemDefault if it's + // the default app; otherwise return useHelperApp. + if (gApplicationsPane.isValidHandlerApp(preferredApp)) { + if (defaultApp && defaultApp.equals(preferredApp)) + return Ci.nsIHandlerInfo.useSystemDefault; + + return Ci.nsIHandlerInfo.useHelperApp; + } + + // The pref is set to "reader", but we don't have a valid preferred app. + // What do we do now? Not sure this is the best option (perhaps we + // should direct the user to the default app, if any), but for now let's + // direct the user to live bookmarks. + return Ci.nsIHandlerInfo.handleInternally; + } + + // If the action is "ask", then alwaysAskBeforeHandling will override + // the action, so it doesn't matter what we say it is, it just has to be + // something that doesn't cause the controller to hide the type. + case "ask": + default: + return Ci.nsIHandlerInfo.handleInternally; + } + }, + + set preferredAction(aNewValue) { + switch (aNewValue) { + + case Ci.nsIHandlerInfo.handleInternally: + this.element(this._prefSelectedReader).value = "bookmarks"; + break; + + case Ci.nsIHandlerInfo.useHelperApp: + this.element(this._prefSelectedAction).value = "reader"; + // The controller has already set preferredApplicationHandler + // to the new helper app. + break; + + case Ci.nsIHandlerInfo.useSystemDefault: + this.element(this._prefSelectedAction).value = "reader"; + this.preferredApplicationHandler = this._defaultApplicationHandler; + break; + } + }, + + get alwaysAskBeforeHandling() { + return this.element(this._prefSelectedAction).value == "ask"; + }, + + set alwaysAskBeforeHandling(aNewValue) { + if (aNewValue == true) + this.element(this._prefSelectedAction).value = "ask"; + else + this.element(this._prefSelectedAction).value = "reader"; + }, + + // 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, specifically when we change + // the action for the feed type, which results in feed preference updates, + // which spawn "pref changed" notifications that would otherwise cause us + // to rebuild the view unnecessarily. + _storingAction: false, + + + // nsIMIMEInfo + + get primaryExtension() { + return "xml"; + }, + + + // Storage + + // Changes to the preferred action and handler take effect immediately + // (we write them out to the preferences right as they happen), + // so we when the controller calls store() after modifying the handlers, + // the only thing we need to store is the removal of possible handlers + // XXX Should we hold off on making the changes until this method gets called? + store: function() { + for (let app of this._possibleApplicationHandlers._removed) { + if (app instanceof Ci.nsILocalHandlerApp) { + let pref = this.element(PREF_FEED_SELECTED_APP); + var preferredAppFile = pref.value; + if (preferredAppFile) { + let preferredApp = getLocalHandlerApp(preferredAppFile); + if (app.equals(preferredApp)) + pref.reset(); + } + } + else { + app.QueryInterface(Ci.nsIWebContentHandlerInfo); + this._converterSvc.removeContentHandler(app.contentType, app.uri); + } + } + this._possibleApplicationHandlers._removed = []; + }, + + + // Icons + + get smallIcon() { + return this._smallIcon; + } + +}; + +var feedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_FEED), + _prefSelectedApp: PREF_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/feedIcon16.png", + _appPrefLabel: "webFeed" +} + +var videoFeedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_VIDEO_FEED), + _prefSelectedApp: PREF_VIDEO_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_VIDEO_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_VIDEO_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_VIDEO_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/videoFeedIcon16.png", + _appPrefLabel: "videoPodcastFeed" +} + +var audioFeedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_AUDIO_FEED), + _prefSelectedApp: PREF_AUDIO_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_AUDIO_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_AUDIO_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_AUDIO_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/audioFeedIcon16.png", + _appPrefLabel: "audioPodcastFeed" +} + +/** + * InternalHandlerInfoWrapper provides a basic mechanism to create an internal + * mime type handler that can be enabled/disabled in the applications preference + * menu. + */ +function InternalHandlerInfoWrapper(aMIMEType) { + var mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + var handlerInfo = mimeSvc.getFromTypeAndExtension(aMIMEType, null); + + HandlerInfoWrapper.call(this, aMIMEType, handlerInfo); +} + +InternalHandlerInfoWrapper.prototype = { + __proto__: HandlerInfoWrapper.prototype, + + // Override store so we so we can notify any code listening for registration + // or unregistration of this handler. + store: function() { + HandlerInfoWrapper.prototype.store.call(this); + Services.obs.notifyObservers(null, this._handlerChanged, null); + }, + + get enabled() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + get description() { + return this.element("bundlePreferences").getString(this._appPrefLabel); + } +}; + +var pdfHandlerInfo = { + __proto__: new InternalHandlerInfoWrapper(TYPE_PDF), + _handlerChanged: TOPIC_PDFJS_HANDLER_CHANGED, + _appPrefLabel: "portableDocumentFormat", + get enabled() { + return !Services.prefs.getBoolPref(PREF_PDFJS_DISABLED); + }, +}; + + +// Prefpane Controller + +var gApplicationsPane = { + // 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: [], + + // A count of the number of times each visible type description appears. + // We use these counts to determine whether or not to annotate descriptions + // with their types to distinguish duplicate descriptions from each other. + // A hash of integer counts, indexed by string description. + _visibleTypeDescriptionCount: {}, + + + // Convenience & Performance Shortcuts + + // These get defined by init(). + _brandShortName : null, + _prefsBundle : null, + _list : null, + _filter : null, + + _prefSvc : Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch), + + _mimeSvc : Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService), + + _helperAppSvc : Cc["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Ci.nsIExternalHelperAppService), + + _handlerSvc : Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService), + + _ioSvc : Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService), + + + // Initialization & Destruction + + init: function() { + function setEventListener(aId, aEventType, aCallback) + { + document.getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gApplicationsPane)); + } + + // Initialize shortcuts to some commonly accessed elements & values. + this._brandShortName = + document.getElementById("bundleBrand").getString("brandShortName"); + this._prefsBundle = document.getElementById("bundlePreferences"); + this._list = document.getElementById("handlersView"); + this._filter = document.getElementById("filter"); + + // Observe preferences that influence what we display so we can rebuild + // the view when they change. + this._prefSvc.addObserver(PREF_SHOW_PLUGINS_IN_LIST, this, false); + this._prefSvc.addObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_READER, this, false); + + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_READER, this, false); + + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_READER, this, false); + + + setEventListener("focusSearch1", "command", gApplicationsPane.focusFilterBox); + setEventListener("focusSearch2", "command", gApplicationsPane.focusFilterBox); + setEventListener("filter", "command", gApplicationsPane.filter); + setEventListener("handlersView", "select", + gApplicationsPane.onSelectionChanged); + setEventListener("typeColumn", "click", gApplicationsPane.sort); + setEventListener("actionColumn", "click", gApplicationsPane.sort); + + // Listen for window unload so we can remove our preference observers. + window.addEventListener("unload", this, false); + + // 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"); + + // Load the data and build the list of handlers. + // By doing this in a timeout, we let the preferences dialog resize itself + // to an appropriate size before we add a bunch of items to the list. + // Otherwise, if there are many items, and the Applications prefpane + // is the one that gets displayed when the user first opens the dialog, + // the dialog might stretch too much in an attempt to fit them all in. + // XXX Shouldn't we perhaps just set a max-height on the richlistbox? + var _delayedPaneLoad = function(self) { + self._loadData(); + self._rebuildVisibleTypes(); + self._sortVisibleTypes(); + self._rebuildView(); + + // Notify observers that the UI is now ready + Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService). + notifyObservers(window, "app-handler-pane-loaded", null); + } + setTimeout(_delayedPaneLoad, 0, this); + }, + + destroy: function() { + window.removeEventListener("unload", this, false); + this._prefSvc.removeObserver(PREF_SHOW_PLUGINS_IN_LIST, this); + this._prefSvc.removeObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_READER, this); + + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_READER, this); + + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_READER, this); + }, + + + // nsISupports + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIObserver) || + aIID.equals(Ci.nsIDOMEventListener || + aIID.equals(Ci.nsISupports))) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // nsIObserver + + observe: function (aSubject, aTopic, aData) { + // Rebuild the list when there are changes to preferences that influence + // whether or not to show certain entries in the list. + if (aTopic == "nsPref:changed" && !this._storingAction) { + // These two prefs alter the list of visible types, so we have to rebuild + // that list when they change. + if (aData == PREF_SHOW_PLUGINS_IN_LIST || + aData == PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS) { + this._rebuildVisibleTypes(); + this._sortVisibleTypes(); + } + + // All the prefs we observe can affect what we display, so we rebuild + // the view when any of them changes. + this._rebuildView(); + } + }, + + + // nsIDOMEventListener + + handleEvent: function(aEvent) { + if (aEvent.type == "unload") { + this.destroy(); + } + }, + + + // Composed Model Construction + + _loadData: function() { + this._loadFeedHandler(); + this._loadInternalHandlers(); + this._loadPluginHandlers(); + this._loadApplicationHandlers(); + }, + + _loadFeedHandler: function() { + this._handledTypes[TYPE_MAYBE_FEED] = feedHandlerInfo; + feedHandlerInfo.handledOnlyByPlugin = false; + + this._handledTypes[TYPE_MAYBE_VIDEO_FEED] = videoFeedHandlerInfo; + videoFeedHandlerInfo.handledOnlyByPlugin = false; + + this._handledTypes[TYPE_MAYBE_AUDIO_FEED] = audioFeedHandlerInfo; + audioFeedHandlerInfo.handledOnlyByPlugin = false; + }, + + /** + * Load higher level internal handlers so they can be turned on/off in the + * applications menu. + */ + _loadInternalHandlers: function() { + var internalHandlers = [pdfHandlerInfo]; + for (let internalHandler of internalHandlers) { + if (internalHandler.enabled) { + this._handledTypes[internalHandler.type] = internalHandler; + } + } + }, + + /** + * Load the set of handlers defined by plugins. + * + * Note: if there's more than one plugin for a given MIME type, we assume + * the last one is the one that the application will use. That may not be + * correct, but it's how we've been doing it for years. + * + * Perhaps we should instead query navigator.mimeTypes for the set of types + * supported by the application and then get the plugin from each MIME type's + * enabledPlugin property. But if there's a plugin for a type, we need + * to know about it even if it isn't enabled, since we're going to give + * the user an option to enable it. + * + * Also note that enabledPlugin does not get updated when + * plugin.disable_full_page_plugin_for_types changes, so even if we could use + * enabledPlugin to get the plugin that would be used, we'd still need to + * check the pref ourselves to find out if it's enabled. + */ + _loadPluginHandlers: function() { + "use strict"; + + let mimeTypes = navigator.mimeTypes; + + for (let mimeType of mimeTypes) { + let handlerInfoWrapper; + if (mimeType.type in this._handledTypes) { + handlerInfoWrapper = this._handledTypes[mimeType.type]; + } else { + let wrappedHandlerInfo = + this._mimeSvc.getFromTypeAndExtension(mimeType.type, null); + handlerInfoWrapper = new HandlerInfoWrapper(mimeType.type, wrappedHandlerInfo); + handlerInfoWrapper.handledOnlyByPlugin = true; + this._handledTypes[mimeType.type] = handlerInfoWrapper; + } + handlerInfoWrapper.pluginName = mimeType.enabledPlugin.name; + } + }, + + /** + * Load the set of handlers defined by the application datastore. + */ + _loadApplicationHandlers: function() { + var wrappedHandlerInfos = this._handlerSvc.enumerate(); + while (wrappedHandlerInfos.hasMoreElements()) { + let wrappedHandlerInfo = + wrappedHandlerInfos.getNext().QueryInterface(Ci.nsIHandlerInfo); + let type = wrappedHandlerInfo.type; + + let handlerInfoWrapper; + if (type in this._handledTypes) + handlerInfoWrapper = this._handledTypes[type]; + else { + handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo); + this._handledTypes[type] = handlerInfoWrapper; + } + + handlerInfoWrapper.handledOnlyByPlugin = false; + } + }, + + + // View Construction + + _rebuildVisibleTypes: function() { + // Reset the list of visible types and the visible type description counts. + this._visibleTypes = []; + this._visibleTypeDescriptionCount = {}; + + // Get the preferences that help determine what types to show. + var showPlugins = this._prefSvc.getBoolPref(PREF_SHOW_PLUGINS_IN_LIST); + var hidePluginsWithoutExtensions = + this._prefSvc.getBoolPref(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS); + + for (let type in this._handledTypes) { + let handlerInfo = this._handledTypes[type]; + + // Hide plugins without associated extensions if so prefed so we don't + // show a whole bunch of obscure types handled by plugins on Mac. + // Note: though protocol types don't have extensions, we still show them; + // the pref is only meant to be applied to MIME types, since plugins are + // only associated with MIME types. + // FIXME: should we also check the "suffixes" property of the plugin? + // Filed as bug 395135. + if (hidePluginsWithoutExtensions && handlerInfo.handledOnlyByPlugin && + handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + !handlerInfo.primaryExtension) + continue; + + // Hide types handled only by plugins if so prefed. + if (handlerInfo.handledOnlyByPlugin && !showPlugins) + continue; + + // We couldn't find any reason to exclude the type, so include it. + this._visibleTypes.push(handlerInfo); + + if (handlerInfo.description in this._visibleTypeDescriptionCount) + this._visibleTypeDescriptionCount[handlerInfo.description]++; + else + this._visibleTypeDescriptionCount[handlerInfo.description] = 1; + } + }, + + _rebuildView: function() { + // Clear the list of entries. + while (this._list.childNodes.length > 1) + this._list.removeChild(this._list.lastChild); + + var visibleTypes = this._visibleTypes; + + // If the user is filtering the list, then only show matching types. + if (this._filter.value) + visibleTypes = visibleTypes.filter(this._matchesFilter, this); + + for (let visibleType of visibleTypes) { + let item = document.createElement("richlistitem"); + item.setAttribute("type", visibleType.type); + item.setAttribute("typeDescription", this._describeType(visibleType)); + if (visibleType.smallIcon) + item.setAttribute("typeIcon", visibleType.smallIcon); + item.setAttribute("actionDescription", + this._describePreferredAction(visibleType)); + + if (!this._setIconClassForPreferredAction(visibleType, item)) { + item.setAttribute("actionIcon", + this._getIconURLForPreferredAction(visibleType)); + } + + this._list.appendChild(item); + } + + this._selectLastSelectedType(); + }, + + _matchesFilter: function(aType) { + var filterValue = this._filter.value.toLowerCase(); + return this._describeType(aType).toLowerCase().indexOf(filterValue) != -1 || + this._describePreferredAction(aType).toLowerCase().indexOf(filterValue) != -1; + }, + + /** + * Describe, in a human-readable fashion, the type represented by the given + * handler info object. Normally this is just the description provided by + * the info object, but if more than one object presents the same description, + * then we annotate the duplicate descriptions with the type itself to help + * users distinguish between those types. + * + * @param aHandlerInfo {nsIHandlerInfo} the type being described + * @returns {string} a description of the type + */ + _describeType: function(aHandlerInfo) { + if (this._visibleTypeDescriptionCount[aHandlerInfo.description] > 1) + return this._prefsBundle.getFormattedString("typeDescriptionWithType", + [aHandlerInfo.description, + aHandlerInfo.type]); + + return aHandlerInfo.description; + }, + + /** + * Describe, in a human-readable fashion, the preferred action to take on + * the type represented by the given handler info object. + * + * XXX Should this be part of the HandlerInfoWrapper interface? It would + * violate the separation of model and view, but it might make more sense + * nonetheless (f.e. it would make sortTypes easier). + * + * @param aHandlerInfo {nsIHandlerInfo} the type whose preferred action + * is being described + * @returns {string} a description of the action + */ + _describePreferredAction: function(aHandlerInfo) { + // alwaysAskBeforeHandling overrides the preferred action, so if that flag + // is set, then describe that behavior instead. For most types, this is + // the "alwaysAsk" string, but for the feed type we show something special. + if (aHandlerInfo.alwaysAskBeforeHandling) { + if (isFeedType(aHandlerInfo.type)) + return this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + return this._prefsBundle.getString("alwaysAsk"); + } + + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + return this._prefsBundle.getString("saveFile"); + + case Ci.nsIHandlerInfo.useHelperApp: + var preferredApp = aHandlerInfo.preferredApplicationHandler; + var name; + if (preferredApp instanceof Ci.nsILocalHandlerApp) + name = getFileDisplayName(preferredApp.executable); + else + name = preferredApp.name; + return this._prefsBundle.getFormattedString("useApp", [name]); + + case Ci.nsIHandlerInfo.handleInternally: + // For the feed type, handleInternally means live bookmarks. + if (isFeedType(aHandlerInfo.type)) { + return this._prefsBundle.getFormattedString("addLiveBookmarksInApp", + [this._brandShortName]); + } + + if (aHandlerInfo instanceof InternalHandlerInfoWrapper) { + return this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + } + + // For other types, handleInternally looks like either useHelperApp + // or useSystemDefault depending on whether or not there's a preferred + // handler app. + if (this.isValidHandlerApp(aHandlerInfo.preferredApplicationHandler)) + return aHandlerInfo.preferredApplicationHandler.name; + + return aHandlerInfo.defaultDescription; + + // XXX Why don't we say the app will handle the type internally? + // Is it because the app can't actually do that? But if that's true, + // then why would a preferredAction ever get set to this value + // in the first place? + + case Ci.nsIHandlerInfo.useSystemDefault: + return this._prefsBundle.getFormattedString("useDefault", + [aHandlerInfo.defaultDescription]); + + case kActionUsePlugin: + return this._prefsBundle.getFormattedString("usePluginIn", + [aHandlerInfo.pluginName, + this._brandShortName]); + default: + throw new Error(`Unexpected preferredAction: ${aHandlerInfo.preferredAction}`); + } + }, + + _selectLastSelectedType: function() { + // If the list is disabled by the pref.downloads.disable_button.edit_actions + // preference being locked, then don't select the type, as that would cause + // it to appear selected, with a different background and an actions menu + // that makes it seem like you can choose an action for the type. + if (this._list.disabled) + return; + + var lastSelectedType = this._list.getAttribute("lastSelectedType"); + if (!lastSelectedType) + return; + + var item = this._list.getElementsByAttribute("type", lastSelectedType)[0]; + if (!item) + return; + + this._list.selectedItem = item; + }, + + /** + * 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: function(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.nsIWebContentHandlerInfo) + return aHandlerApp.uri; + + return false; + }, + + _isValidHandlerExecutable: function(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: function() { + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + var menu = + document.getAnonymousElementByAttribute(typeItem, "class", "actionsMenu"); + var menuPopup = menu.menupopup; + + // Clear out existing items. + while (menuPopup.hasChildNodes()) + menuPopup.removeChild(menuPopup.lastChild); + + // Add the "Preview in Firefox" option for optional internal handlers. + if (handlerInfo instanceof InternalHandlerInfoWrapper) { + let internalMenuItem = document.createElement("menuitem"); + internalMenuItem.setAttribute("action", Ci.nsIHandlerInfo.handleInternally); + let label = this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + internalMenuItem.setAttribute("label", label); + internalMenuItem.setAttribute("tooltiptext", label); + internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask"); + menuPopup.appendChild(internalMenuItem); + } + + { + var askMenuItem = document.createElement("menuitem"); + askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk); + let label; + if (isFeedType(handlerInfo.type)) + label = this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + else + label = this._prefsBundle.getString("alwaysAsk"); + askMenuItem.setAttribute("label", label); + askMenuItem.setAttribute("tooltiptext", label); + 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, nor is it + // available to feeds, since the feed code doesn't implement the capability. + if ((handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + !isFeedType(handlerInfo.type)) { + var saveMenuItem = document.createElement("menuitem"); + saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk); + let label = this._prefsBundle.getString("saveFile"); + saveMenuItem.setAttribute("label", label); + saveMenuItem.setAttribute("tooltiptext", label); + saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save"); + menuPopup.appendChild(saveMenuItem); + } + + // If this is the feed type, add a Live Bookmarks item. + if (isFeedType(handlerInfo.type)) { + let internalMenuItem = document.createElement("menuitem"); + internalMenuItem.setAttribute("action", Ci.nsIHandlerInfo.handleInternally); + let label = this._prefsBundle.getFormattedString("addLiveBookmarksInApp", + [this._brandShortName]); + internalMenuItem.setAttribute("label", label); + internalMenuItem.setAttribute("tooltiptext", label); + internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "feed"); + menuPopup.appendChild(internalMenuItem); + } + + // Add a separator to distinguish these items from the helper app items + // that follow them. + let menuItem = document.createElement("menuseparator"); + menuPopup.appendChild(menuItem); + + // Create a menu item for the OS default application, if any. + if (handlerInfo.hasDefaultHandler) { + var defaultMenuItem = document.createElement("menuitem"); + defaultMenuItem.setAttribute("action", Ci.nsIHandlerInfo.useSystemDefault); + let label = this._prefsBundle.getFormattedString("useDefault", + [handlerInfo.defaultDescription]); + defaultMenuItem.setAttribute("label", label); + defaultMenuItem.setAttribute("tooltiptext", handlerInfo.defaultDescription); + defaultMenuItem.setAttribute("image", this._getIconURLForSystemDefault(handlerInfo)); + + menuPopup.appendChild(defaultMenuItem); + } + + // Create menu items for possible handlers. + let preferredApp = handlerInfo.preferredApplicationHandler; + let possibleApps = handlerInfo.possibleApplicationHandlers.enumerate(); + var possibleAppMenuItems = []; + while (possibleApps.hasMoreElements()) { + let possibleApp = possibleApps.getNext(); + if (!this.isValidHandlerApp(possibleApp)) + continue; + + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + let label; + if (possibleApp instanceof Ci.nsILocalHandlerApp) + label = getFileDisplayName(possibleApp.executable); + else + label = possibleApp.name; + label = this._prefsBundle.getFormattedString("useApp", [label]); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", 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); + } + + // Create a menu item for the plugin. + if (handlerInfo.pluginName) { + var pluginMenuItem = document.createElement("menuitem"); + pluginMenuItem.setAttribute("action", kActionUsePlugin); + let label = this._prefsBundle.getFormattedString("usePluginIn", + [handlerInfo.pluginName, + this._brandShortName]); + pluginMenuItem.setAttribute("label", label); + pluginMenuItem.setAttribute("tooltiptext", label); + pluginMenuItem.setAttribute(APP_ICON_ATTR_NAME, "plugin"); + menuPopup.appendChild(pluginMenuItem); + } + + // 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.createElement("menuitem"); + menuItem.className = "choose-app-item"; + menuItem.addEventListener("command", function(e) { + gApplicationsPane.chooseApp(e); + }); + let label = this._prefsBundle.getString("useOtherApp"); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", label); + menuPopup.appendChild(menuItem); + } + + // Create a menu item for managing applications. + if (possibleAppMenuItems.length) { + let menuItem = document.createElement("menuseparator"); + menuPopup.appendChild(menuItem); + menuItem = document.createElement("menuitem"); + menuItem.className = "manage-app-item"; + menuItem.addEventListener("command", function(e) { + gApplicationsPane.manageApp(e); + }); + menuItem.setAttribute("label", this._prefsBundle.getString("manageApp")); + 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 switch (handlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.handleInternally: + menu.selectedItem = internalMenuItem; + break; + case Ci.nsIHandlerInfo.useSystemDefault: + menu.selectedItem = defaultMenuItem; + break; + case Ci.nsIHandlerInfo.useHelperApp: + if (preferredApp) + menu.selectedItem = + possibleAppMenuItems.filter(v => v.handlerApp.equals(preferredApp))[0]; + break; + case kActionUsePlugin: + menu.selectedItem = pluginMenuItem; + 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: function (event) { + var column = event.target; + + // If the user clicked on a new sort column, remove the direction indicator + // from the old column. + if (this._sortColumn && this._sortColumn != column) + this._sortColumn.removeAttribute("sortDirection"); + + this._sortColumn = column; + + // Set (or switch) the sort direction indicator. + if (column.getAttribute("sortDirection") == "ascending") + column.setAttribute("sortDirection", "descending"); + else + column.setAttribute("sortDirection", "ascending"); + + this._sortVisibleTypes(); + this._rebuildView(); + }, + + /** + * Sort the list of visible types by the current sort column/direction. + */ + _sortVisibleTypes: function() { + if (!this._sortColumn) + return; + + var t = this; + + function sortByType(a, b) { + return t._describeType(a).toLowerCase(). + localeCompare(t._describeType(b).toLowerCase()); + } + + function sortByAction(a, b) { + return t._describePreferredAction(a).toLowerCase(). + localeCompare(t._describePreferredAction(b).toLowerCase()); + } + + switch (this._sortColumn.getAttribute("value")) { + case "type": + this._visibleTypes.sort(sortByType); + break; + case "action": + this._visibleTypes.sort(sortByAction); + break; + } + + if (this._sortColumn.getAttribute("sortDirection") == "descending") + this._visibleTypes.reverse(); + }, + + /** + * Filter the list when the user enters a filter term into the filter field. + */ + filter: function() { + this._rebuildView(); + }, + + focusFilterBox: function() { + this._filter.focus(); + this._filter.select(); + }, + + + // Changes + + onSelectAction: function(aActionItem) { + this._storingAction = true; + + try { + this._storeAction(aActionItem); + } + finally { + this._storingAction = false; + } + }, + + _storeAction: function(aActionItem) { + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + + let action = parseInt(aActionItem.getAttribute("action")); + + // Set the plugin state if we're enabling or disabling a plugin. + if (action == kActionUsePlugin) + handlerInfo.enablePluginType(); + else if (handlerInfo.pluginName && !handlerInfo.isDisabledPluginType) + handlerInfo.disablePluginType(); + + // 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(); + + // Make sure the handler info object is flagged to indicate that there is + // now some user configuration for the type. + handlerInfo.handledOnlyByPlugin = false; + + // Update the action label and image to reflect the new preferred action. + typeItem.setAttribute("actionDescription", + this._describePreferredAction(handlerInfo)); + if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) { + typeItem.setAttribute("actionIcon", + this._getIconURLForPreferredAction(handlerInfo)); + } + }, + + manageApp: function(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + + 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 + typeItem.setAttribute("actionDescription", + this._describePreferredAction(handlerInfo)); + if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) { + typeItem.setAttribute("actionIcon", + this._getIconURLForPreferredAction(handlerInfo)); + } + }; + + gSubDialog.open("chrome://browser/content/preferences/applicationManager.xul", + "resizable=no", handlerInfo, onComplete); + + }, + + chooseApp: function(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 = function(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 = + document.getAnonymousElementByAttribute(typeItem, "class", "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; + } + } + } + }.bind(this); + + if (AppConstants.platform == "win") { + var params = {}; + var handlerInfo = this._handledTypes[this._list.selectedItem.type]; + + if (isFeedType(handlerInfo.type)) { + // MIME info will be null, create a temp object. + params.mimeInfo = this._mimeSvc.getFromTypeAndExtension(handlerInfo.type, + handlerInfo.primaryExtension); + } else { + params.mimeInfo = handlerInfo.wrappedHandlerInfo; + } + + params.title = this._prefsBundle.getString("fpTitleChooseApp"); + params.description = handlerInfo.description; + 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.xul", + null, params, onAppSelected); + } else { + let winTitle = this._prefsBundle.getString("fpTitleChooseApp"); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(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 handlerInfo = this._handledTypes[this._list.selectedItem.type]; + handlerInfo.addPossibleApplicationHandler(handlerApp); + + chooseAppCallback(handlerApp); + } + }.bind(this); + + // 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); + } + }, + + // Mark which item in the list was last selected so we can reselect it + // when we rebuild the list or when the user returns to the prefpane. + onSelectionChanged: function() { + if (this._list.selectedItem) + this._list.setAttribute("lastSelectedType", + this._list.selectedItem.getAttribute("type")); + }, + + _setIconClassForPreferredAction: function(aHandlerInfo, aElement) { + // If this returns true, the attribute that CSS sniffs for was set to something + // so you shouldn't manually set an icon URI. + // This removes the existing actionIcon attribute if any, even if returning false. + aElement.removeAttribute("actionIcon"); + + if (aHandlerInfo.alwaysAskBeforeHandling) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "ask"); + return true; + } + + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + aElement.setAttribute(APP_ICON_ATTR_NAME, "save"); + return true; + + case Ci.nsIHandlerInfo.handleInternally: + if (isFeedType(aHandlerInfo.type)) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "feed"); + return true; + } else if (aHandlerInfo instanceof InternalHandlerInfoWrapper) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "ask"); + return true; + } + break; + + case kActionUsePlugin: + aElement.setAttribute(APP_ICON_ATTR_NAME, "plugin"); + return true; + } + aElement.removeAttribute(APP_ICON_ATTR_NAME); + return false; + }, + + _getIconURLForPreferredAction: function(aHandlerInfo) { + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.useSystemDefault: + return this._getIconURLForSystemDefault(aHandlerInfo); + + case Ci.nsIHandlerInfo.useHelperApp: + let preferredApp = aHandlerInfo.preferredApplicationHandler; + if (this.isValidHandlerApp(preferredApp)) + return this._getIconURLForHandlerApp(preferredApp); + // Explicit fall-through + + // This should never happen, but if preferredAction is set to some weird + // value, then fall back to the generic application icon. + default: + return ICON_URL_APP; + } + }, + + _getIconURLForHandlerApp: function(aHandlerApp) { + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) + return this._getIconURLForFile(aHandlerApp.executable); + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) + return this._getIconURLForWebApp(aHandlerApp.uriTemplate); + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) + return this._getIconURLForWebApp(aHandlerApp.uri) + + // We know nothing about other kinds of handler apps. + return ""; + }, + + _getIconURLForFile: function(aFile) { + var fph = this._ioSvc.getProtocolHandler("file"). + QueryInterface(Ci.nsIFileProtocolHandler); + var urlSpec = fph.getURLSpecFromFile(aFile); + + return "moz-icon://" + urlSpec + "?size=16"; + }, + + _getIconURLForWebApp: function(aWebAppURITemplate) { + var uri = this._ioSvc.newURI(aWebAppURITemplate, null, null); + + // 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) && this._prefSvc.getBoolPref("browser.chrome.favicons")) + return uri.prePath + "/favicon.ico"; + + return ""; + }, + + _getIconURLForSystemDefault: function(aHandlerInfo) { + // 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 ("wrappedHandlerInfo" in aHandlerInfo) { + let wrappedHandlerInfo = aHandlerInfo.wrappedHandlerInfo; + + if (wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + wrappedHandlerInfo instanceof Ci.nsIPropertyBag) { + try { + let url = 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; + } + +}; diff --git a/browser/components/preferences/in-content/applications.xul b/browser/components/preferences/in-content/applications.xul new file mode 100644 index 000000000..1d4723493 --- /dev/null +++ b/browser/components/preferences/in-content/applications.xul @@ -0,0 +1,95 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<!-- Applications panel --> + +<script type="application/javascript" + src="chrome://browser/content/preferences/in-content/applications.js"/> + +<preferences id="feedsPreferences" hidden="true" data-category="paneApplications"> + <preference id="browser.feeds.handler" + name="browser.feeds.handler" + type="string"/> + <preference id="browser.feeds.handler.default" + name="browser.feeds.handler.default" + type="string"/> + <preference id="browser.feeds.handlers.application" + name="browser.feeds.handlers.application" + type="file"/> + <preference id="browser.feeds.handlers.webservice" + name="browser.feeds.handlers.webservice" + type="string"/> + + <preference id="browser.videoFeeds.handler" + name="browser.videoFeeds.handler" + type="string"/> + <preference id="browser.videoFeeds.handler.default" + name="browser.videoFeeds.handler.default" + type="string"/> + <preference id="browser.videoFeeds.handlers.application" + name="browser.videoFeeds.handlers.application" + type="file"/> + <preference id="browser.videoFeeds.handlers.webservice" + name="browser.videoFeeds.handlers.webservice" + type="string"/> + + <preference id="browser.audioFeeds.handler" + name="browser.audioFeeds.handler" + type="string"/> + <preference id="browser.audioFeeds.handler.default" + name="browser.audioFeeds.handler.default" + type="string"/> + <preference id="browser.audioFeeds.handlers.application" + name="browser.audioFeeds.handlers.application" + type="file"/> + <preference id="browser.audioFeeds.handlers.webservice" + name="browser.audioFeeds.handlers.webservice" + type="string"/> + + <preference id="pref.downloads.disable_button.edit_actions" + name="pref.downloads.disable_button.edit_actions" + type="bool"/> +</preferences> + +<keyset data-category="paneApplications"> + <!-- Ctrl+f/k focus the search box in the Applications pane. + These <key>s have oncommand attributes because of bug 371900. --> + <key key="&focusSearch1.key;" modifiers="accel" id="focusSearch1" oncommand=";"/> + <key key="&focusSearch2.key;" modifiers="accel" id="focusSearch2" oncommand=";"/> +</keyset> + +<hbox id="header-applications" + class="header" + hidden="true" + data-category="paneApplications"> + <label class="header-name" flex="1">&paneApplications.title;</label> + <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a> +</hbox> + +<vbox id="applicationsContent" + data-category="paneApplications" + hidden="true" + flex="1"> + <hbox> + <textbox id="filter" flex="1" + type="search" + placeholder="&filter.emptytext;" + aria-controls="handlersView"/> + </hbox> + + <separator class="thin"/> + + <richlistbox id="handlersView" orient="vertical" persist="lastSelectedType" + preference="pref.downloads.disable_button.edit_actions" + flex="1"> + <listheader equalsize="always"> + <treecol id="typeColumn" label="&typeColumn.label;" value="type" + accesskey="&typeColumn.accesskey;" persist="sortDirection" + flex="1" sortDirection="ascending"/> + <treecol id="actionColumn" label="&actionColumn2.label;" value="action" + accesskey="&actionColumn2.accesskey;" persist="sortDirection" + flex="1"/> + </listheader> + </richlistbox> +</vbox> diff --git a/browser/components/preferences/in-content/containers.js b/browser/components/preferences/in-content/containers.js new file mode 100644 index 000000000..758e45fff --- /dev/null +++ b/browser/components/preferences/in-content/containers.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/AppConstants.jsm"); +Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm"); + +const containersBundle = Services.strings.createBundle("chrome://browser/locale/preferences/containers.properties"); + +const defaultContainerIcon = "fingerprint"; +const defaultContainerColor = "blue"; + +let gContainersPane = { + + init() { + this._list = document.getElementById("containersView"); + + document.getElementById("backContainersLink").addEventListener("click", function () { + gotoPref("privacy"); + }); + + this._rebuildView(); + }, + + _rebuildView() { + const containers = ContextualIdentityService.getIdentities(); + while (this._list.firstChild) { + this._list.firstChild.remove(); + } + for (let container of containers) { + let item = document.createElement("richlistitem"); + item.setAttribute("containerName", ContextualIdentityService.getUserContextLabel(container.userContextId)); + item.setAttribute("containerIcon", container.icon); + item.setAttribute("containerColor", container.color); + item.setAttribute("userContextId", container.userContextId); + + this._list.appendChild(item); + } + }, + + onRemoveClick(button) { + let userContextId = button.getAttribute("value"); + ContextualIdentityService.remove(userContextId); + this._rebuildView(); + }, + onPeferenceClick(button) { + this.openPreferenceDialog(button.getAttribute("value")); + }, + + onAddButtonClick(button) { + this.openPreferenceDialog(null); + }, + + openPreferenceDialog(userContextId) { + let identity = { + name: "", + icon: defaultContainerIcon, + color: defaultContainerColor + }; + let title; + if (userContextId) { + identity = ContextualIdentityService.getIdentityFromId(userContextId); + // This is required to get the translation string from defaults + identity.name = ContextualIdentityService.getUserContextLabel(identity.userContextId); + title = containersBundle.formatStringFromName("containers.updateContainerTitle", [identity.name], 1); + } + + const params = { userContextId, identity, windowTitle: title }; + gSubDialog.open("chrome://browser/content/preferences/containers.xul", + null, params); + } + +}; diff --git a/browser/components/preferences/in-content/containers.xul b/browser/components/preferences/in-content/containers.xul new file mode 100644 index 000000000..e83bac1c3 --- /dev/null +++ b/browser/components/preferences/in-content/containers.xul @@ -0,0 +1,54 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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 type="application/javascript" + src="chrome://browser/content/preferences/in-content/containers.js"/> + +<preferences id="containerPreferences" hidden="true" data-category="paneContainer"> + <!-- Containers --> + <preference id="privacy.userContext.enabled" + name="privacy.userContext.enabled" + type="bool"/> + +</preferences> + +<hbox hidden="true" + class="container-header-links" + data-category="paneContainers"> + <label class="text-link" id="backContainersLink" value="&backLink.label;" /> +</hbox> + +<hbox id="header-containers" + class="header" + hidden="true" + data-category="paneContainers"> + <label class="header-name" flex="1">&paneContainers.title;</label> + <button class="help-button" + aria-label="&helpButton.label;"/> +</hbox> + +<!-- Containers --> +<groupbox id="browserContainersGroup" data-category="paneContainers" hidden="true"> + <vbox id="browserContainersbox"> + + <richlistbox id="containersView" orient="vertical" persist="lastSelectedType" + flex="1"> + <listheader equalsize="always"> + <treecol id="typeColumn" label="&label.label;" value="type" + persist="sortDirection" + flex="1" sortDirection="ascending"/> + <treecol id="actionColumn" value="action" + persist="sortDirection" + flex="1"/> + </listheader> + </richlistbox> + </vbox> + <vbox> + <hbox flex="1"> + <button onclick="gContainersPane.onAddButtonClick();" accesskey="&addButton.accesskey;" label="&addButton.label;"/> + </hbox> + </vbox> +</groupbox> diff --git a/browser/components/preferences/in-content/content.js b/browser/components/preferences/in-content/content.js new file mode 100644 index 000000000..5ba334b02 --- /dev/null +++ b/browser/components/preferences/in-content/content.js @@ -0,0 +1,294 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +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; + } +}); + +var gContentPane = { + init: function () + { + function setEventListener(aId, aEventType, aCallback) + { + document.getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gContentPane)); + } + + // Initializes the fonts dropdowns displayed in this pane. + this._rebuildFonts(); + var menulist = document.getElementById("defaultFont"); + if (menulist.selectedIndex == -1) { + menulist.value = FontBuilder.readFontSelection(menulist); + } + + // Show translation preferences if we may: + const prefName = "browser.translation.ui.show"; + if (Services.prefs.getBoolPref(prefName)) { + let row = document.getElementById("translationBox"); + row.removeAttribute("hidden"); + // Showing attribution only for Bing Translator. + Components.utils.import("resource:///modules/translation/Translation.jsm"); + if (Translation.translationEngine == "bing") { + document.getElementById("bingAttribution").removeAttribute("hidden"); + } + } + + if (AlertsServiceDND) { + let notificationsDoNotDisturbRow = + document.getElementById("notificationsDoNotDisturbRow"); + notificationsDoNotDisturbRow.removeAttribute("hidden"); + if (AlertsServiceDND.manualDoNotDisturb) { + let notificationsDoNotDisturb = + document.getElementById("notificationsDoNotDisturb"); + notificationsDoNotDisturb.setAttribute("checked", true); + } + } + + setEventListener("font.language.group", "change", + gContentPane._rebuildFonts); + setEventListener("notificationsPolicyButton", "command", + gContentPane.showNotificationExceptions); + setEventListener("popupPolicyButton", "command", + gContentPane.showPopupExceptions); + setEventListener("advancedFonts", "command", + gContentPane.configureFonts); + setEventListener("colors", "command", + gContentPane.configureColors); + setEventListener("chooseLanguage", "command", + gContentPane.showLanguages); + setEventListener("translationAttributionImage", "click", + gContentPane.openTranslationProviderAttribution); + setEventListener("translateButton", "command", + gContentPane.showTranslationExceptions); + setEventListener("notificationsDoNotDisturb", "command", + gContentPane.toggleDoNotDisturbNotifications); + + let notificationInfoURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + "push"; + document.getElementById("notificationsPolicyLearnMore").setAttribute("href", + notificationInfoURL); + + let drmInfoURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content"; + document.getElementById("playDRMContentLink").setAttribute("href", drmInfoURL); + let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled"); + // Force-disable/hide on WinXP: + if (navigator.platform.toLowerCase().startsWith("win")) { + emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6; + } + if (!emeUIEnabled) { + // Don't want to rely on .hidden for the toplevel groupbox because + // of the pane hiding/showing code potentially interfering: + document.getElementById("drmGroup").setAttribute("style", "display: none !important"); + } + }, + + // UTILITY FUNCTIONS + + /** + * Utility function to enable/disable the button specified by aButtonID based + * on the value of the Boolean preference specified by aPreferenceID. + */ + updateButtons: function (aButtonID, aPreferenceID) + { + var button = document.getElementById(aButtonID); + var preference = document.getElementById(aPreferenceID); + button.disabled = preference.value != true; + return undefined; + }, + + // BEGIN UI CODE + + /* + * Preferences: + * + * dom.disable_open_during_load + * - true if popups are blocked by default, false otherwise + */ + + // NOTIFICATIONS + + /** + * Displays the notifications exceptions dialog where specific site notification + * preferences can be set. + */ + showNotificationExceptions() + { + let bundlePreferences = document.getElementById("bundlePreferences"); + let params = { permissionType: "desktop-notification" }; + params.windowTitle = bundlePreferences.getString("notificationspermissionstitle"); + params.introText = bundlePreferences.getString("notificationspermissionstext4"); + + gSubDialog.open("chrome://browser/content/preferences/permissions.xul", + "resizable=yes", params); + + try { + Services.telemetry + .getHistogramById("WEB_NOTIFICATION_EXCEPTIONS_OPENED").add(); + } catch (e) {} + }, + + + // POP-UPS + + /** + * Displays the popup exceptions dialog where specific site popup preferences + * can be set. + */ + showPopupExceptions: function () + { + var bundlePreferences = document.getElementById("bundlePreferences"); + var params = { blockVisible: false, sessionVisible: false, allowVisible: true, + prefilledHost: "", permissionType: "popup" } + params.windowTitle = bundlePreferences.getString("popuppermissionstitle"); + params.introText = bundlePreferences.getString("popuppermissionstext"); + + gSubDialog.open("chrome://browser/content/preferences/permissions.xul", + "resizable=yes", params); + }, + + // FONTS + + /** + * Populates the default font list in UI. + */ + _rebuildFonts: function () + { + var preferences = document.getElementById("contentPreferences"); + // Ensure preferences are "visible" to ensure bindings work. + preferences.hidden = false; + // Force flush: + preferences.clientHeight; + var langGroupPref = document.getElementById("font.language.group"); + this._selectDefaultLanguageGroup(langGroupPref.value, + this._readDefaultFontTypeForLanguage(langGroupPref.value) == "serif"); + }, + + /** + * + */ + _selectDefaultLanguageGroup: function (aLanguageGroup, aIsSerif) + { + 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 preferences = document.getElementById("contentPreferences"); + 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 = document.getElementById(prefs[i].format.replace(/%LANG%/, aLanguageGroup)); + if (!preference) { + preference = document.createElement("preference"); + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + preference.id = name; + preference.setAttribute("name", name); + preference.setAttribute("type", prefs[i].type); + preferences.appendChild(preference); + } + + if (!prefs[i].element) + continue; + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) + FontBuilder.buildFontList(aLanguageGroup, prefs[i].fonttype, element); + + preference.setElementValue(element); + } + } + }, + + /** + * Returns the type of the current default font for the language denoted by + * aLanguageGroup. + */ + _readDefaultFontTypeForLanguage: function (aLanguageGroup) + { + const kDefaultFontType = "font.default.%LANG%"; + var defaultFontTypePref = kDefaultFontType.replace(/%LANG%/, aLanguageGroup); + var preference = document.getElementById(defaultFontTypePref); + if (!preference) { + preference = document.createElement("preference"); + preference.id = defaultFontTypePref; + preference.setAttribute("name", defaultFontTypePref); + preference.setAttribute("type", "string"); + preference.setAttribute("onchange", "gContentPane._rebuildFonts();"); + document.getElementById("contentPreferences").appendChild(preference); + } + return preference.value; + }, + + /** + * Displays the fonts dialog, where web page font names and sizes can be + * configured. + */ + configureFonts: function () + { + gSubDialog.open("chrome://browser/content/preferences/fonts.xul", "resizable=no"); + }, + + /** + * Displays the colors dialog, where default web page/link/etc. colors can be + * configured. + */ + configureColors: function () + { + gSubDialog.open("chrome://browser/content/preferences/colors.xul", "resizable=no"); + }, + + // LANGUAGES + + /** + * Shows a dialog in which the preferred language for web content may be set. + */ + showLanguages: function () + { + gSubDialog.open("chrome://browser/content/preferences/languages.xul"); + }, + + /** + * Displays the translation exceptions dialog where specific site and language + * translation preferences can be set. + */ + showTranslationExceptions: function () + { + gSubDialog.open("chrome://browser/content/preferences/translation.xul"); + }, + + openTranslationProviderAttribution: function () + { + Components.utils.import("resource:///modules/translation/Translation.jsm"); + Translation.openProviderAttribution(); + }, + + toggleDoNotDisturbNotifications: function (event) + { + AlertsServiceDND.manualDoNotDisturb = event.target.checked; + }, +}; diff --git a/browser/components/preferences/in-content/content.xul b/browser/components/preferences/in-content/content.xul new file mode 100644 index 000000000..c646c16a2 --- /dev/null +++ b/browser/components/preferences/in-content/content.xul @@ -0,0 +1,209 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<!-- Content panel --> + +<preferences id="contentPreferences" hidden="true" data-category="paneContent"> + + <!-- DRM content --> + <preference id="media.eme.enabled" + name="media.eme.enabled" + type="bool"/> + + <!-- Popups --> + <preference id="dom.disable_open_during_load" + name="dom.disable_open_during_load" + type="bool"/> + + <!-- Fonts --> + <preference id="font.language.group" + name="font.language.group" + type="wstring"/> + + <!-- Languages --> + <preference id="browser.translation.detectLanguage" + name="browser.translation.detectLanguage" + type="bool"/> +</preferences> + +<script type="application/javascript" + src="chrome://mozapps/content/preferences/fontbuilder.js"/> +<script type="application/javascript" + src="chrome://browser/content/preferences/in-content/content.js"/> + +<hbox id="header-content" + class="header" + hidden="true" + data-category="paneContent"> + <label class="header-name" flex="1">&paneContent.title;</label> + <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a> +</hbox> + +<groupbox id="drmGroup" data-category="paneContent" hidden="true"> + <caption><label>&drmContent.label;</label></caption> + <grid id="contentGrid2"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows id="contentRows-2"> + <row id="playDRMContentRow"> + <vbox align="start"> + <checkbox id="playDRMContent" preference="media.eme.enabled" + label="&playDRMContent.label;" accesskey="&playDRMContent.accesskey;"/> + </vbox> + <hbox pack="end" align="center"> + <label id="playDRMContentLink" class="text-link" value="&playDRMContent.learnMore.label;"/> + </hbox> + </row> + </rows> + </grid> +</groupbox> + +<groupbox id="notificationsGroup" data-category="paneContent" hidden="true"> + <caption><label>¬ificationsPolicy.label;</label></caption> + <grid> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows> + <row id="notificationsPolicyRow" align="center"> + <hbox align="start"> + <label id="notificationsPolicy">¬ificationsPolicyDesc3.label;</label> + <label id="notificationsPolicyLearnMore" + class="text-link" + value="¬ificationsPolicyLearnMore.label;"/> + </hbox> + <hbox pack="end"> + <button id="notificationsPolicyButton" label="¬ificationsPolicyButton.label;" + accesskey="¬ificationsPolicyButton.accesskey;"/> + </hbox> + </row> + <row id="notificationsDoNotDisturbRow" hidden="true"> + <vbox align="start"> + <checkbox id="notificationsDoNotDisturb" label="¬ificationsDoNotDisturb.label;" + accesskey="¬ificationsDoNotDisturb.accesskey;"/> + <label id="notificationsDoNotDisturbDetails" + class="indent" + value="¬ificationsDoNotDisturbDetails.value;"/> + </vbox> + </row> + </rows> + </grid> +</groupbox> + +<groupbox id="miscGroup" data-category="paneContent" hidden="true"> + <caption><label>&popups.label;</label></caption> + <grid id="contentGrid"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows id="contentRows-1"> + <row id="popupPolicyRow"> + <vbox align="start"> + <checkbox id="popupPolicy" preference="dom.disable_open_during_load" + label="&blockPopups.label;" accesskey="&blockPopups.accesskey;" + onsyncfrompreference="return gContentPane.updateButtons('popupPolicyButton', + 'dom.disable_open_during_load');"/> + </vbox> + <hbox pack="end"> + <button id="popupPolicyButton" label="&popupExceptions.label;" + accesskey="&popupExceptions.accesskey;"/> + </hbox> + </row> + </rows> + </grid> +</groupbox> + +<!-- Fonts and Colors --> +<groupbox id="fontsGroup" data-category="paneContent" hidden="true"> + <caption><label>&fontsAndColors.label;</label></caption> + + <grid id="fontsGrid"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows id="fontsRows"> + <row id="fontRow"> + <hbox align="center"> + <label control="defaultFont" accesskey="&defaultFont.accesskey;">&defaultFont.label;</label> + <menulist id="defaultFont" delayprefsave="true"/> + <label id="defaultFontSizeLabel" control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</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" icon="select-font" + label="&advancedFonts.label;" + accesskey="&advancedFonts.accesskey;"/> + </row> + <row id="colorsRow"> + <hbox/> + <button id="colors" icon="select-color" + label="&colors.label;" + accesskey="&colors.accesskey;"/> + </row> + </rows> + </grid> +</groupbox> + +<!-- Languages --> +<groupbox id="languagesGroup" data-category="paneContent" hidden="true"> + <caption><label>&languages.label;</label></caption> + + <hbox id="languagesBox" align="center"> + <description flex="1" control="chooseLanguage">&chooseLanguage.label;</description> + <button id="chooseLanguage" + label="&chooseButton.label;" + accesskey="&chooseButton.accesskey;"/> + </hbox> + + <hbox id="translationBox" hidden="true"> + <hbox align="center" flex="1"> + <checkbox id="translate" preference="browser.translation.detectLanguage" + label="&translateWebPages.label;." accesskey="&translateWebPages.accesskey;" + onsyncfrompreference="return gContentPane.updateButtons('translateButton', + 'browser.translation.detectLanguage');"/> + <hbox id="bingAttribution" hidden="true"> + <label>&translation.options.attribution.beforeLogo;</label> + <separator orient="vertical" class="thin"/> + <image id="translationAttributionImage" aria-label="Microsoft Translator" + src="chrome://browser/content/microsoft-translator-attribution.png"/> + <separator orient="vertical" class="thin"/> + <label>&translation.options.attribution.afterLogo;</label> + </hbox> + </hbox> + <button id="translateButton" label="&translateExceptions.label;" + accesskey="&translateExceptions.accesskey;"/> + </hbox> +</groupbox> diff --git a/browser/components/preferences/in-content/jar.mn b/browser/components/preferences/in-content/jar.mn new file mode 100644 index 000000000..52f536e96 --- /dev/null +++ b/browser/components/preferences/in-content/jar.mn @@ -0,0 +1,18 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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/in-content/preferences.js +* content/browser/preferences/in-content/preferences.xul + content/browser/preferences/in-content/subdialogs.js + + content/browser/preferences/in-content/main.js + content/browser/preferences/in-content/privacy.js + content/browser/preferences/in-content/containers.js + content/browser/preferences/in-content/advanced.js + content/browser/preferences/in-content/applications.js + content/browser/preferences/in-content/content.js + content/browser/preferences/in-content/sync.js + content/browser/preferences/in-content/security.js + content/browser/preferences/in-content/search.js diff --git a/browser/components/preferences/in-content/main.js b/browser/components/preferences/in-content/main.js new file mode 100644 index 000000000..4f20ba8c3 --- /dev/null +++ b/browser/components/preferences/in-content/main.js @@ -0,0 +1,721 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/Downloads.jsm"); +Components.utils.import("resource://gre/modules/FileUtils.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); +Components.utils.import("resource:///modules/ShellService.jsm"); +Components.utils.import("resource:///modules/TransientPrefs.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +if (AppConstants.E10S_TESTING_ONLY) { + XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); +} + +var gMainPane = { + /** + * Initialization of this. + */ + init: function () + { + function setEventListener(aId, aEventType, aCallback) + { + document.getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gMainPane)); + } + + if (AppConstants.HAVE_SHELL_SERVICE) { + this.updateSetDefaultBrowser(); + if (AppConstants.platform == "win") { + // In Windows 8 we launch the control panel since it's the only + // way to get all file type association prefs. So we don't know + // when the user will select the default. We refresh here periodically + // in case the default changes. On other Windows OS's defaults can also + // be set while the prefs are open. + window.setInterval(this.updateSetDefaultBrowser.bind(this), 1000); + } + } + + // set up the "use current page" label-changing listener + this._updateUseCurrentButton(); + window.addEventListener("focus", this._updateUseCurrentButton.bind(this), false); + + this.updateBrowserStartupLastSession(); + + if (AppConstants.platform == "win") { + // Functionality for "Show tabs in taskbar" on Windows 7 and up. + try { + let sysInfo = Cc["@mozilla.org/system-info;1"]. + getService(Ci.nsIPropertyBag2); + let ver = parseFloat(sysInfo.getProperty("version")); + let showTabsInTaskbar = document.getElementById("showTabsInTaskbar"); + showTabsInTaskbar.hidden = ver < 6.1; + } catch (ex) {} + } + + // The "closing multiple tabs" and "opening multiple tabs might slow down + // &brandShortName;" warnings provide options for not showing these + // warnings again. When the user disabled them, we provide checkboxes to + // re-enable the warnings. + if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnClose")) + document.getElementById("warnCloseMultiple").hidden = true; + if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen")) + document.getElementById("warnOpenMany").hidden = true; + + setEventListener("browser.privatebrowsing.autostart", "change", + gMainPane.updateBrowserStartupLastSession); + setEventListener("browser.download.dir", "change", + gMainPane.displayDownloadDirPref); + if (AppConstants.HAVE_SHELL_SERVICE) { + setEventListener("setDefaultButton", "command", + gMainPane.setDefaultBrowser); + } + setEventListener("useCurrent", "command", + gMainPane.setHomePageToCurrent); + setEventListener("useBookmark", "command", + gMainPane.setHomePageToBookmark); + setEventListener("restoreDefaultHomePage", "command", + gMainPane.restoreDefaultHomePage); + setEventListener("chooseFolder", "command", + gMainPane.chooseFolder); + + if (AppConstants.E10S_TESTING_ONLY) { + setEventListener("e10sAutoStart", "command", + gMainPane.enableE10SChange); + let e10sCheckbox = document.getElementById("e10sAutoStart"); + + let e10sPref = document.getElementById("browser.tabs.remote.autostart"); + let e10sTempPref = document.getElementById("e10sTempPref"); + let e10sForceEnable = document.getElementById("e10sForceEnable"); + + let preffedOn = e10sPref.value || e10sTempPref.value || e10sForceEnable.value; + + if (preffedOn) { + // The checkbox is checked if e10s is preffed on and enabled. + e10sCheckbox.checked = Services.appinfo.browserTabsRemoteAutostart; + + // but if it's force disabled, then the checkbox is disabled. + e10sCheckbox.disabled = !Services.appinfo.browserTabsRemoteAutostart; + } + } + + if (AppConstants.MOZ_DEV_EDITION) { + let uAppData = OS.Constants.Path.userApplicationDataDir; + let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile"); + + setEventListener("separateProfileMode", "command", gMainPane.separateProfileModeChange); + let separateProfileModeCheckbox = document.getElementById("separateProfileMode"); + setEventListener("getStarted", "click", gMainPane.onGetStarted); + + OS.File.stat(ignoreSeparateProfile).then(() => separateProfileModeCheckbox.checked = false, + () => separateProfileModeCheckbox.checked = true); + } + + // Notify observers that the UI is now ready + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "main-pane-loaded", null); + }, + + enableE10SChange: function () + { + if (AppConstants.E10S_TESTING_ONLY) { + let e10sCheckbox = document.getElementById("e10sAutoStart"); + let e10sPref = document.getElementById("browser.tabs.remote.autostart"); + let e10sTempPref = document.getElementById("e10sTempPref"); + + let prefsToChange; + if (e10sCheckbox.checked) { + // Enabling e10s autostart + prefsToChange = [e10sPref]; + } else { + // Disabling e10s autostart + prefsToChange = [e10sPref]; + if (e10sTempPref.value) { + prefsToChange.push(e10sTempPref); + } + } + + let buttonIndex = confirmRestartPrompt(e10sCheckbox.checked, 0, + true, false); + if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) { + for (let prefToChange of prefsToChange) { + prefToChange.value = e10sCheckbox.checked; + } + + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + } + + // Revert the checkbox in case we didn't quit + e10sCheckbox.checked = e10sPref.value || e10sTempPref.value; + } + }, + + separateProfileModeChange: function () + { + if (AppConstants.MOZ_DEV_EDITION) { + function quitApp() { + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestartNotSameProfile); + } + function revertCheckbox(error) { + separateProfileModeCheckbox.checked = !separateProfileModeCheckbox.checked; + if (error) { + Cu.reportError("Failed to toggle separate profile mode: " + error); + } + } + function createOrRemoveSpecialDevEditionFile(onSuccess) { + let uAppData = OS.Constants.Path.userApplicationDataDir; + let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile"); + + if (separateProfileModeCheckbox.checked) { + OS.File.remove(ignoreSeparateProfile).then(onSuccess, revertCheckbox); + } else { + OS.File.writeAtomic(ignoreSeparateProfile, new Uint8Array()).then(onSuccess, revertCheckbox); + } + } + + let separateProfileModeCheckbox = document.getElementById("separateProfileMode"); + let button_index = confirmRestartPrompt(separateProfileModeCheckbox.checked, + 0, false, true); + switch (button_index) { + case CONFIRM_RESTART_PROMPT_CANCEL: + revertCheckbox(); + return; + case CONFIRM_RESTART_PROMPT_RESTART_NOW: + const Cc = Components.classes, Ci = Components.interfaces; + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", + "restart"); + if (!cancelQuit.data) { + createOrRemoveSpecialDevEditionFile(quitApp); + return; + } + + // Revert the checkbox in case we didn't quit + revertCheckbox(); + return; + case CONFIRM_RESTART_PROMPT_RESTART_LATER: + createOrRemoveSpecialDevEditionFile(); + return; + } + } + }, + + onGetStarted: function (aEvent) { + if (AppConstants.MOZ_DEV_EDITION) { + const Cc = Components.classes, Ci = Components.interfaces; + let wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + let win = wm.getMostRecentWindow("navigator:browser"); + + if (win) { + let accountsTab = win.gBrowser.addTab("about:accounts?action=signin&entrypoint=dev-edition-setup"); + win.gBrowser.selectedTab = accountsTab; + } + } + }, + + // 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.startup.page + * - what page(s) to show when the user starts the application, as an integer: + * + * 0: a blank page + * 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. + */ + + syncFromHomePref: function () + { + let homePref = document.getElementById("browser.startup.homepage"); + + // If the pref is set to about:home or about:newtab, set the value to "" + // to show the placeholder text (about:home title) rather than + // exposing those URLs to users. + let defaultBranch = Services.prefs.getDefaultBranch(""); + let defaultValue = defaultBranch.getComplexValue("browser.startup.homepage", + Ci.nsIPrefLocalizedString).data; + let currentValue = homePref.value.toLowerCase(); + if (currentValue == "about:home" || + (currentValue == defaultValue && currentValue == "about:newtab")) { + return ""; + } + + // If the pref is actually "", show about:blank. The actual home page + // loading code treats them the same, and we don't want the placeholder text + // to be shown. + if (homePref.value == "") + return "about:blank"; + + // Otherwise, show the actual pref value. + return undefined; + }, + + syncToHomePref: function (value) + { + // If the value is "", use about:home. + if (value == "") + return "about:home"; + + // Otherwise, use the actual textbox value. + return undefined; + }, + + /** + * Sets the home page to the current displayed page (or frontmost tab, if the + * most recent browser window contains multiple tabs), updating preference + * window UI to reflect this. + */ + setHomePageToCurrent: function () + { + let homePage = document.getElementById("browser.startup.homepage"); + let tabs = this._getTabsForHomePage(); + function getTabURI(t) { + return t.linkedBrowser.currentURI.spec; + } + + // FIXME Bug 244192: using dangerous "|" joiner! + if (tabs.length) + homePage.value = tabs.map(getTabURI).join("|"); + }, + + /** + * 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: function () + { + var rv = { urls: null, names: null }; + gSubDialog.open("chrome://browser/content/preferences/selectBookmark.xul", + "resizable=yes, modal=yes", rv, + this._setHomePageToBookmarkClosed.bind(this, rv)); + }, + + _setHomePageToBookmarkClosed: function(rv, aEvent) { + if (aEvent.detail.button != "accept") + return; + if (rv.urls && rv.names) { + var homePage = document.getElementById("browser.startup.homepage"); + + // XXX still using dangerous "|" joiner! + homePage.value = rv.urls.join("|"); + } + }, + + /** + * Switches the "Use Current Page" button between its singular and plural + * forms. + */ + _updateUseCurrentButton: function () { + let useCurrent = document.getElementById("useCurrent"); + + + let tabs = this._getTabsForHomePage(); + + if (tabs.length > 1) + useCurrent.label = useCurrent.getAttribute("label2"); + else + useCurrent.label = useCurrent.getAttribute("label1"); + + // In this case, the button's disabled state is set by preferences.xml. + let prefName = "pref.browser.homepage.disable_button.current_page"; + if (document.getElementById(prefName).locked) + return; + + useCurrent.disabled = !tabs.length + }, + + _getTabsForHomePage: function () + { + var win; + var tabs = []; + + const Cc = Components.classes, Ci = Components.interfaces; + var wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + win = wm.getMostRecentWindow("navigator:browser"); + + if (win && win.document.documentElement + .getAttribute("windowtype") == "navigator:browser") { + // We should only include visible & non-pinned tabs + + tabs = win.gBrowser.visibleTabs.slice(win.gBrowser._numPinnedTabs); + tabs = tabs.filter(this.isNotAboutPreferences); + } + + return tabs; + }, + + /** + * Check to see if a tab is not about:preferences + */ + isNotAboutPreferences: function (aElement, aIndex, aArray) + { + return !aElement.linkedBrowser.currentURI.spec.startsWith("about:preferences"); + }, + + /** + * Restores the default home page as the user's home page. + */ + restoreDefaultHomePage: function () + { + var homePage = document.getElementById("browser.startup.homepage"); + homePage.value = homePage.defaultValue; + }, + + // DOWNLOADS + + /* + * Preferences: + * + * browser.download.useDownloadDir - bool + * True - Save files directly to the folder configured via the + * browser.download.folderList preference. + * False - Always ask the user where to save a file and default to + * browser.download.lastDir when displaying a folder picker dialog. + * browser.download.dir - local file handle + * A local folder the user may have selected for downloaded files to be + * saved. Migration of other browser settings may also set this path. + * This folder is enabled when folderList equals 2. + * browser.download.lastDir - local file handle + * May contain the last folder path accessed when the user browsed + * via the file save-as dialog. (see contentAreaUtils.js) + * browser.download.folderList - int + * Indicates the location users wish to save downloaded files too. + * It is also used to display special file labels when the default + * download location is either the Desktop or the Downloads folder. + * Values: + * 0 - The desktop is the default download location. + * 1 - The system's downloads folder is the default download location. + * 2 - The default download location is elsewhere as specified in + * browser.download.dir. + * browser.download.downloadDir + * deprecated. + * browser.download.defaultFolder + * deprecated. + */ + + /** + * Enables/disables the folder field and Browse button based on whether a + * default download directory is being used. + */ + readUseDownloadDir: function () + { + var downloadFolder = document.getElementById("downloadFolder"); + var chooseFolder = document.getElementById("chooseFolder"); + var preference = document.getElementById("browser.download.useDownloadDir"); + downloadFolder.disabled = !preference.value || preference.locked; + chooseFolder.disabled = !preference.value || preference.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(Components.utils.reportError); + }, + chooseFolderTask: Task.async(function* () + { + let bundlePreferences = document.getElementById("bundlePreferences"); + let title = bundlePreferences.getString("chooseDownloadFolderTitle"); + let folderListPref = document.getElementById("browser.download.folderList"); + let currentDirPref = yield this._indexToFolder(folderListPref.value); + let defDownloads = yield this._indexToFolder(1); + let fp = Components.classes["@mozilla.org/filepicker;1"]. + createInstance(Components.interfaces.nsIFilePicker); + + fp.init(window, title, Components.interfaces.nsIFilePicker.modeGetFolder); + fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll); + // First try to open what's currently configured + if (currentDirPref && currentDirPref.exists()) { + fp.displayDirectory = currentDirPref; + } // Try the system's download dir + else if (defDownloads && defDownloads.exists()) { + fp.displayDirectory = defDownloads; + } // Fall back to Desktop + else { + fp.displayDirectory = yield this._indexToFolder(0); + } + + let result = yield new Promise(resolve => fp.open(resolve)); + if (result != Components.interfaces.nsIFilePicker.returnOK) { + return; + } + + let downloadDirPref = document.getElementById("browser.download.dir"); + downloadDirPref.value = fp.file; + folderListPref.value = yield 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(Components.utils.reportError); + + // don't override the preference's value in UI + return undefined; + }, + + displayDownloadDirPrefTask: Task.async(function* () + { + var folderListPref = document.getElementById("browser.download.folderList"); + var bundlePreferences = document.getElementById("bundlePreferences"); + var downloadFolder = document.getElementById("downloadFolder"); + var currentDirPref = document.getElementById("browser.download.dir"); + + // Used in defining the correct path to the folder icon. + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var fph = ios.getProtocolHandler("file") + .QueryInterface(Components.interfaces.nsIFileProtocolHandler); + var iconUrlSpec; + + // Display a 'pretty' label or the path in the UI. + if (folderListPref.value == 2) { + // Custom path selected and is configured + downloadFolder.label = this._getDisplayNameOfFile(currentDirPref.value); + iconUrlSpec = fph.getURLSpecFromFile(currentDirPref.value); + } else if (folderListPref.value == 1) { + // 'Downloads' + // In 1.5, this pointed to a folder we created called 'My Downloads' + // and was available as an option in the 1.5 drop down. On XP this + // was in My Documents, on OSX it was in User Docs. In 2.0, we did + // away with the drop down option, although the special label was + // still supported for the folder if it existed. Because it was + // not exposed it was rarely used. + // With 3.0, a new desktop folder - 'Downloads' was introduced for + // platforms and versions that don't support a default system downloads + // folder. See nsDownloadManager for details. + downloadFolder.label = bundlePreferences.getString("downloadsFolderName"); + iconUrlSpec = fph.getURLSpecFromFile(yield this._indexToFolder(1)); + } else { + // 'Desktop' + downloadFolder.label = bundlePreferences.getString("desktopFolderName"); + iconUrlSpec = fph.getURLSpecFromFile(yield this._getDownloadsFolder("Desktop")); + } + downloadFolder.image = "moz-icon://" + iconUrlSpec + "?size=16"; + }), + + /** + * Returns the textual path of a folder in readable form. + */ + _getDisplayNameOfFile: function (aFolder) + { + // TODO: would like to add support for 'Downloads on Macintosh HD' + // for OS X users. + return aFolder ? aFolder.path : ""; + }, + + /** + * 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" + */ + _getDownloadsFolder: Task.async(function* (aFolder) + { + switch (aFolder) { + case "Desktop": + var fileLoc = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + return fileLoc.get("Desk", Components.interfaces.nsILocalFile); + case "Downloads": + let downloadsDir = yield Downloads.getSystemDownloadsDirectory(); + return new FileUtils.File(downloadsDir); + } + throw "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 + */ + _folderToIndex: Task.async(function* (aFolder) + { + if (!aFolder || aFolder.equals(yield this._getDownloadsFolder("Desktop"))) + return 0; + else if (aFolder.equals(yield 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: Task.async(function* (aIndex) + { + switch (aIndex) { + case 0: + return yield this._getDownloadsFolder("Desktop"); + case 1: + return yield this._getDownloadsFolder("Downloads"); + } + var currentDirPref = document.getElementById("browser.download.dir"); + return currentDirPref.value; + }), + + /** + * Hide/show the "Show my windows and tabs from last time" option based + * on the value of the browser.privatebrowsing.autostart pref. + */ + updateBrowserStartupLastSession: function() + { + let pbAutoStartPref = document.getElementById("browser.privatebrowsing.autostart"); + let startupPref = document.getElementById("browser.startup.page"); + let menu = document.getElementById("browserStartupPage"); + let option = document.getElementById("browserStartupLastSession"); + if (pbAutoStartPref.value) { + option.setAttribute("disabled", "true"); + if (option.selected) { + menu.selectedItem = document.getElementById("browserStartupHomePage"); + } + } else { + option.removeAttribute("disabled"); + startupPref.updateElements(); // select the correct index in the startup menulist + } + }, + + // TABS + + /* + * Preferences: + * + * browser.link.open_newwindow - int + * Determines where links targeting new windows should open. + * Values: + * 1 - Open in the current window or tab. + * 2 - Open in a new window. + * 3 - Open in a new tab in the most recent window. + * browser.tabs.loadInBackground - bool + * True - Whether browser should switch to a new tab opened from a link. + * browser.tabs.warnOnClose - bool + * True - If when closing a window with multiple tabs the user is warned and + * allowed to cancel the action, false to just close the window. + * browser.tabs.warnOnOpen - bool + * True - Whether the user should be warned when trying to open a lot of + * tabs at once (e.g. a large folder of bookmarks), allowing to + * cancel the action. + * browser.taskbar.previews.enable - bool + * True - Tabs are to be shown in Windows 7 taskbar. + * False - Only the window is to be shown in Windows 7 taskbar. + */ + + /** + * Determines where a link which opens a new window will open. + * + * @returns |true| if such links should be opened in new tabs + */ + readLinkTarget: function() { + var openNewWindow = document.getElementById("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: function() { + 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: function() + { + if (AppConstants.HAVE_SHELL_SERVICE) { + let shellSvc = getShellService(); + let defaultBrowserBox = document.getElementById("defaultBrowserBox"); + if (!shellSvc) { + defaultBrowserBox.hidden = true; + return; + } + let setDefaultPane = document.getElementById("setDefaultPane"); + let isDefault = shellSvc.isDefaultBrowser(false, true); + setDefaultPane.selectedIndex = isDefault ? 1 : 0; + let alwaysCheck = document.getElementById("alwaysCheckDefault"); + alwaysCheck.disabled = alwaysCheck.disabled || + isDefault && alwaysCheck.checked; + } + }, + + /** + * Set browser as the operating system default browser. + */ + setDefaultBrowser: function() + { + if (AppConstants.HAVE_SHELL_SERVICE) { + let alwaysCheckPref = document.getElementById("browser.shell.checkDefaultBrowser"); + alwaysCheckPref.value = true; + + let shellSvc = getShellService(); + if (!shellSvc) + return; + try { + shellSvc.setDefaultBrowser(true, false); + } catch (ex) { + Cu.reportError(ex); + return; + } + + let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0; + document.getElementById("setDefaultPane").selectedIndex = selectedIndex; + } + }, +}; diff --git a/browser/components/preferences/in-content/main.xul b/browser/components/preferences/in-content/main.xul new file mode 100644 index 000000000..526bbc714 --- /dev/null +++ b/browser/components/preferences/in-content/main.xul @@ -0,0 +1,301 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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 type="application/javascript" + src="chrome://browser/content/preferences/in-content/main.js"/> + +<preferences id="mainPreferences" hidden="true" data-category="paneGeneral"> + +#ifdef E10S_TESTING_ONLY + <preference id="browser.tabs.remote.autostart" + name="browser.tabs.remote.autostart" + type="bool"/> + <preference id="e10sTempPref" + name="browser.tabs.remote.autostart.2" + type="bool"/> + <preference id="e10sForceEnable" + name="browser.tabs.remote.force-enable" + type="bool"/> +#endif + + <!-- Startup --> + <preference id="browser.startup.page" + name="browser.startup.page" + type="int"/> + <preference id="browser.startup.homepage" + name="browser.startup.homepage" + type="wstring"/> + +#ifdef HAVE_SHELL_SERVICE + <preference id="browser.shell.checkDefaultBrowser" + name="browser.shell.checkDefaultBrowser" + type="bool"/> + + <preference id="pref.general.disable_button.default_browser" + name="pref.general.disable_button.default_browser" + type="bool"/> +#endif + + <preference id="pref.browser.homepage.disable_button.current_page" + name="pref.browser.homepage.disable_button.current_page" + type="bool"/> + <preference id="pref.browser.homepage.disable_button.bookmark_page" + name="pref.browser.homepage.disable_button.bookmark_page" + type="bool"/> + <preference id="pref.browser.homepage.disable_button.restore_default" + name="pref.browser.homepage.disable_button.restore_default" + type="bool"/> + + <preference id="browser.privatebrowsing.autostart" + name="browser.privatebrowsing.autostart" + type="bool"/> + + <!-- Downloads --> + <preference id="browser.download.useDownloadDir" + name="browser.download.useDownloadDir" + type="bool"/> + + <preference id="browser.download.folderList" + name="browser.download.folderList" + type="int"/> + <preference id="browser.download.dir" + name="browser.download.dir" + type="file"/> + <!-- Tab preferences + Preferences: + + browser.link.open_newwindow + 1 opens such links in the most recent window or tab, + 2 opens such links in a new window, + 3 opens such links in a new tab + browser.tabs.loadInBackground + - true if display should switch to a new tab which has been opened from a + link, false if display shouldn't switch + browser.tabs.warnOnClose + - true if when closing a window with multiple tabs the user is warned and + allowed to cancel the action, false to just close the window + browser.tabs.warnOnOpen + - true if the user should be warned if he attempts to open a lot of tabs at + once (e.g. a large folder of bookmarks), false otherwise + browser.taskbar.previews.enable + - true if tabs are to be shown in the Windows 7 taskbar + --> + + <preference id="browser.link.open_newwindow" + name="browser.link.open_newwindow" + type="int"/> + <preference id="browser.tabs.loadInBackground" + name="browser.tabs.loadInBackground" + type="bool" + inverted="true"/> + <preference id="browser.tabs.warnOnClose" + name="browser.tabs.warnOnClose" + type="bool"/> + <preference id="browser.tabs.warnOnOpen" + name="browser.tabs.warnOnOpen" + type="bool"/> + <preference id="browser.sessionstore.restore_on_demand" + name="browser.sessionstore.restore_on_demand" + type="bool"/> +#ifdef XP_WIN + <preference id="browser.taskbar.previews.enable" + name="browser.taskbar.previews.enable" + type="bool"/> +#endif + <preference id="browser.ctrlTab.previews" + name="browser.ctrlTab.previews" + type="bool"/> +</preferences> + +<hbox id="header-general" + class="header" + hidden="true" + data-category="paneGeneral"> + <label class="header-name" flex="1">&paneGeneral.title;</label> + <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a> +</hbox> + +<!-- Startup --> +<groupbox id="startupGroup" + data-category="paneGeneral" + hidden="true"> + <caption><label>&startup.label;</label></caption> + +#ifdef MOZ_DEV_EDITION + <vbox id="separateProfileBox"> + <checkbox id="separateProfileMode" + label="&separateProfileMode.label;"/> + <hbox align="center" class="indent"> + <label id="useFirefoxSync">&useFirefoxSync.label;</label> + <label id="getStarted" class="text-link">&getStarted.label;</label> + </hbox> + </vbox> +#endif + +#ifdef E10S_TESTING_ONLY + <checkbox id="e10sAutoStart" + label="&e10sEnabled.label;"/> +#endif + +#ifdef HAVE_SHELL_SERVICE + <vbox id="defaultBrowserBox"> + <hbox align="center"> + <checkbox id="alwaysCheckDefault" preference="browser.shell.checkDefaultBrowser" + label="&alwaysCheckDefault2.label;" accesskey="&alwaysCheckDefault2.accesskey;"/> + </hbox> + <deck id="setDefaultPane"> + <hbox align="center" class="indent"> + <label id="isNotDefaultLabel" flex="1">&isNotDefault.label;</label> + <button id="setDefaultButton" + label="&setAsMyDefaultBrowser2.label;" accesskey="&setAsMyDefaultBrowser2.accesskey;" + preference="pref.general.disable_button.default_browser"/> + </hbox> + <hbox align="center" class="indent"> + <label id="isDefaultLabel" flex="1">&isDefault.label;</label> + </hbox> + </deck> + <separator class="thin"/> + </vbox> +#endif + + <html:table id="startupTable"> + <html:tr> + <html:td class="label-cell"> + <label accesskey="&startupPage.accesskey;" + control="browserStartupPage">&startupPage.label;</label> + </html:td> + <html:td class="content-cell"> + <menulist id="browserStartupPage" + class="content-cell-item" + preference="browser.startup.page"> + <menupopup> + <menuitem label="&startupHomePage.label;" + value="1" + id="browserStartupHomePage"/> + <menuitem label="&startupBlankPage.label;" + value="0" + id="browserStartupBlank"/> + <menuitem label="&startupLastSession.label;" + value="3" + id="browserStartupLastSession"/> + </menupopup> + </menulist> + </html:td> + </html:tr> + <html:tr> + <html:td class="label-cell"> + <label accesskey="&homepage.accesskey;" + control="browserHomePage">&homepage.label;</label> + </html:td> + <html:td class="content-cell"> + <textbox id="browserHomePage" + class="padded uri-element content-cell-item" + type="autocomplete" + autocompletesearch="unifiedcomplete" + onsyncfrompreference="return gMainPane.syncFromHomePref();" + onsynctopreference="return gMainPane.syncToHomePref(this.value);" + placeholder="&abouthome.pageTitle;" + preference="browser.startup.homepage"/> + </html:td> + </html:tr> + <html:tr> + <html:td class="label-cell" /> + <html:td class="content-cell homepage-buttons"> + <button id="useCurrent" + class="content-cell-item" + label="" + accesskey="&useCurrentPage.accesskey;" + label1="&useCurrentPage.label;" + label2="&useMultiple.label;" + preference="pref.browser.homepage.disable_button.current_page"/> + <button id="useBookmark" + class="content-cell-item" + label="&chooseBookmark.label;" + accesskey="&chooseBookmark.accesskey;" + preference="pref.browser.homepage.disable_button.bookmark_page"/> + <button id="restoreDefaultHomePage" + class="content-cell-item" + label="&restoreDefault.label;" + accesskey="&restoreDefault.accesskey;" + preference="pref.browser.homepage.disable_button.restore_default"/> + </html:td> + </html:tr> + </html:table> +</groupbox> + +<!-- Downloads --> +<groupbox id="downloadsGroup" + data-category="paneGeneral" + hidden="true"> + <caption><label>&downloads.label;</label></caption> + + <radiogroup id="saveWhere" + preference="browser.download.useDownloadDir" + onsyncfrompreference="return gMainPane.readUseDownloadDir();"> + <hbox id="saveToRow"> + <radio id="saveTo" + value="true" + label="&saveTo.label;" + accesskey="&saveTo.accesskey;" + aria-labelledby="saveTo downloadFolder"/> + <filefield id="downloadFolder" + flex="1" + preference="browser.download.folderList" + preference-editable="true" + aria-labelledby="saveTo" + onsyncfrompreference="return gMainPane.displayDownloadDirPref();"/> + <button id="chooseFolder" +#ifdef XP_MACOSX + accesskey="&chooseFolderMac.accesskey;" + label="&chooseFolderMac.label;" +#else + accesskey="&chooseFolderWin.accesskey;" + label="&chooseFolderWin.label;" +#endif + /> + </hbox> + <hbox> + <radio id="alwaysAsk" + value="false" + label="&alwaysAsk.label;" + accesskey="&alwaysAsk.accesskey;"/> + </hbox> + </radiogroup> +</groupbox> + +<!-- Tab preferences --> +<groupbox data-category="paneGeneral" + hidden="true" align="start"> + <caption><label>&tabsGroup.label;</label></caption> + + <checkbox id="ctrlTabRecentlyUsedOrder" label="&ctrlTabRecentlyUsedOrder.label;" + accesskey="&ctrlTabRecentlyUsedOrder.accesskey;" + preference="browser.ctrlTab.previews"/> + + <checkbox id="linkTargeting" label="&newWindowsAsTabs.label;" + accesskey="&newWindowsAsTabs.accesskey;" + preference="browser.link.open_newwindow" + onsyncfrompreference="return gMainPane.readLinkTarget();" + onsynctopreference="return gMainPane.writeLinkTarget();"/> + + <checkbox id="warnCloseMultiple" label="&warnCloseMultipleTabs.label;" + accesskey="&warnCloseMultipleTabs.accesskey;" + preference="browser.tabs.warnOnClose"/> + + <checkbox id="warnOpenMany" label="&warnOpenManyTabs.label;" + accesskey="&warnOpenManyTabs.accesskey;" + preference="browser.tabs.warnOnOpen"/> + + <checkbox id="switchToNewTabs" label="&switchToNewTabs.label;" + accesskey="&switchToNewTabs.accesskey;" + preference="browser.tabs.loadInBackground"/> + +#ifdef XP_WIN + <checkbox id="showTabsInTaskbar" label="&showTabsInTaskbar.label;" + accesskey="&showTabsInTaskbar.accesskey;" + preference="browser.taskbar.previews.enable"/> +#endif +</groupbox> diff --git a/browser/components/preferences/in-content/moz.build b/browser/components/preferences/in-content/moz.build new file mode 100644 index 000000000..08a75bcf7 --- /dev/null +++ b/browser/components/preferences/in-content/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', 'gtk2', 'gtk3', 'cocoa'): + DEFINES['HAVE_SHELL_SERVICE'] = 1 + +JAR_MANIFESTS += ['jar.mn'] diff --git a/browser/components/preferences/in-content/preferences.js b/browser/components/preferences/in-content/preferences.js new file mode 100644 index 000000000..e18ab4b04 --- /dev/null +++ b/browser/components/preferences/in-content/preferences.js @@ -0,0 +1,315 @@ +/* - This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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 subdialogs.js */ +/* import-globals-from advanced.js */ +/* import-globals-from main.js */ +/* import-globals-from search.js */ +/* import-globals-from content.js */ +/* import-globals-from privacy.js */ +/* import-globals-from applications.js */ +/* import-globals-from security.js */ +/* import-globals-from sync.js */ +/* import-globals-from ../../../base/content/utilityOverlay.js */ + +"use strict"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +var gLastHash = ""; + +var gCategoryInits = new Map(); +function init_category_if_required(category) { + let categoryInfo = gCategoryInits.get(category); + if (!categoryInfo) { + throw "Unknown in-content prefs category! Can't init " + category; + } + if (categoryInfo.inited) { + return; + } + categoryInfo.init(); +} + +function register_module(categoryName, categoryObject) { + gCategoryInits.set(categoryName, { + inited: false, + init: function() { + categoryObject.init(); + this.inited = true; + } + }); +} + +addEventListener("DOMContentLoaded", function onLoad() { + removeEventListener("DOMContentLoaded", onLoad); + init_all(); +}); + +function init_all() { + document.documentElement.instantApply = true; + + gSubDialog.init(); + register_module("paneGeneral", gMainPane); + register_module("paneSearch", gSearchPane); + register_module("panePrivacy", gPrivacyPane); + register_module("paneContainers", gContainersPane); + register_module("paneAdvanced", gAdvancedPane); + register_module("paneApplications", gApplicationsPane); + register_module("paneContent", gContentPane); + register_module("paneSync", gSyncPane); + register_module("paneSecurity", gSecurityPane); + + 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"); + }); + + window.addEventListener("hashchange", onHashChange); + gotoPref(); + + init_dynamic_padding(); + + var initFinished = new CustomEvent("Initialized", { + 'bubbles': true, + 'cancelable': true + }); + document.dispatchEvent(initFinished); + + categories = categories.querySelectorAll("richlistitem.category"); + for (let category of categories) { + let name = internalPrefCategoryNameToFriendlyName(category.value); + let helpSelector = `#header-${name} > .help-button`; + let helpButton = document.querySelector(helpSelector); + helpButton.setAttribute("href", getHelpLinkURL(category.getAttribute("helpTopic"))); + } + + // Wait until initialization of all preferences are complete before + // notifying observers that the UI is now ready. + Services.obs.notifyObservers(window, "advanced-pane-loaded", null); +} + +// Make the space above the categories list shrink on low window heights +function init_dynamic_padding() { + let categories = document.getElementById("categories"); + let catPadding = Number.parseInt(getComputedStyle(categories) + .getPropertyValue('padding-top')); + let fullHeight = categories.lastElementChild.getBoundingClientRect().bottom; + let mediaRule = ` + @media (max-height: ${fullHeight}px) { + #categories { + padding-top: calc(100vh - ${fullHeight - catPadding}px); + } + } + `; + let mediaStyle = document.createElementNS('http://www.w3.org/1999/xhtml', 'html:style'); + mediaStyle.setAttribute('type', 'text/css'); + mediaStyle.appendChild(document.createCDATASection(mediaRule)); + document.documentElement.appendChild(mediaStyle); +} + +function telemetryBucketForCategory(category) { + switch (category) { + case "general": + case "search": + case "content": + case "applications": + case "privacy": + case "security": + case "sync": + return category; + case "advanced": + let advancedPaneTabs = document.getElementById("advancedPrefs"); + switch (advancedPaneTabs.selectedTab.id) { + case "generalTab": + return "advancedGeneral"; + case "dataChoicesTab": + return "advancedDataChoices"; + case "networkTab": + return "advancedNetwork"; + case "updateTab": + return "advancedUpdates"; + case "encryptionTab": + return "advancedCerts"; + } + // fall-through for unknown. + default: + return "unknown"; + } +} + +function onHashChange() { + gotoPref(); +} + +function gotoPref(aCategory) { + let categories = document.getElementById("categories"); + const kDefaultCategoryInternalName = categories.firstElementChild.value; + let hash = document.location.hash; + let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName; + category = friendlyPrefCategoryNameToInternalName(category); + + // Updating the hash (below) or changing the selected category + // will re-enter gotoPref. + if (gLastHash == category) + return; + let item = categories.querySelector(".category[value=" + category + "]"); + if (!item) { + category = kDefaultCategoryInternalName; + item = categories.querySelector(".category[value=" + category + "]"); + } + + try { + init_category_if_required(category); + } catch (ex) { + Cu.reportError("Error initializing preference category " + category + ": " + ex); + throw ex; + } + + let friendlyName = internalPrefCategoryNameToFriendlyName(category); + if (gLastHash || category != kDefaultCategoryInternalName) { + document.location.hash = friendlyName; + } + // Need to set the gLastHash before setting categories.selectedItem since + // the categories 'select' event will re-enter the gotoPref codepath. + gLastHash = category; + categories.selectedItem = item; + window.history.replaceState(category, document.title); + search(category, "data-category"); + let mainContent = document.querySelector(".main-content"); + mainContent.scrollTop = 0; + + Services.telemetry + .getHistogramById("FX_PREFERENCES_CATEGORY_OPENED") + .add(telemetryBucketForCategory(friendlyName)); +} + +function search(aQuery, aAttribute) { + let mainPrefPane = document.getElementById("mainPrefPane"); + let elements = mainPrefPane.children; + for (let element of elements) { + let attributeValue = element.getAttribute(aAttribute); + element.hidden = (attributeValue != aQuery); + } + + 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); + } +} + +function helpButtonCommand() { + let pane = history.state; + let categories = document.getElementById("categories"); + let helpTopic = categories.querySelector(".category[value=" + pane + "]") + .getAttribute("helpTopic"); + openHelpLink(helpTopic); +} + +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; +function confirmRestartPrompt(aRestartToEnable, aDefaultButtonIndex, + aWantRevertAsCancelButton, + aWantRestartLaterButton) { + let brandName = document.getElementById("bundleBrand").getString("brandShortName"); + let bundle = document.getElementById("bundlePreferences"); + let msg = bundle.getFormattedString(aRestartToEnable ? + "featureEnableRequiresRestart" : + "featureDisableRequiresRestart", + [brandName]); + let title = bundle.getFormattedString("shouldRestartTitle", [brandName]); + let prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService); + + // Set up the first (index 0) button: + let button0Text = bundle.getFormattedString("okToRestartButton", [brandName]); + let buttonFlags = (Services.prompt.BUTTON_POS_0 * + Services.prompt.BUTTON_TITLE_IS_STRING); + + + // Set up the second (index 1) button: + let button1Text = null; + if (aWantRevertAsCancelButton) { + button1Text = bundle.getString("revertNoRestartButton"); + buttonFlags += (Services.prompt.BUTTON_POS_1 * + Services.prompt.BUTTON_TITLE_IS_STRING); + } else { + buttonFlags += (Services.prompt.BUTTON_POS_1 * + Services.prompt.BUTTON_TITLE_CANCEL); + } + + // Set up the third (index 2) button: + let button2Text = null; + if (aWantRestartLaterButton) { + button2Text = bundle.getString("restartLater"); + buttonFlags += (Services.prompt.BUTTON_POS_2 * + Services.prompt.BUTTON_TITLE_IS_STRING); + } + + 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 = prompts.confirmEx(window, title, msg, buttonFlags, + button0Text, button1Text, button2Text, + 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; +} diff --git a/browser/components/preferences/in-content/preferences.xul b/browser/components/preferences/in-content/preferences.xul new file mode 100644 index 000000000..e9664eaf4 --- /dev/null +++ b/browser/components/preferences/in-content/preferences.xul @@ -0,0 +1,224 @@ +<?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"?> +<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?> +<?xml-stylesheet + href="chrome://browser/skin/preferences/in-content/preferences.css"?> +<?xml-stylesheet + href="chrome://browser/content/preferences/handlers.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/in-content/search.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/in-content/containers.css"?> + +<!DOCTYPE page [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % globalPreferencesDTD SYSTEM "chrome://global/locale/preferences.dtd"> +<!ENTITY % preferencesDTD SYSTEM + "chrome://browser/locale/preferences/preferences.dtd"> +<!ENTITY % privacyDTD SYSTEM "chrome://browser/locale/preferences/privacy.dtd"> +<!ENTITY % tabsDTD SYSTEM "chrome://browser/locale/preferences/tabs.dtd"> +<!ENTITY % searchDTD SYSTEM "chrome://browser/locale/preferences/search.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncDTD SYSTEM "chrome://browser/locale/preferences/sync.dtd"> +<!ENTITY % securityDTD SYSTEM + "chrome://browser/locale/preferences/security.dtd"> +<!ENTITY % containersDTD SYSTEM + "chrome://browser/locale/preferences/containers.dtd"> +<!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd"> +<!ENTITY % mainDTD SYSTEM "chrome://browser/locale/preferences/main.dtd"> +<!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> +<!ENTITY % contentDTD SYSTEM "chrome://browser/locale/preferences/content.dtd"> +<!ENTITY % applicationsDTD SYSTEM + "chrome://browser/locale/preferences/applications.dtd"> +<!ENTITY % advancedDTD SYSTEM + "chrome://browser/locale/preferences/advanced.dtd"> +%brandDTD; +%globalPreferencesDTD; +%preferencesDTD; +%privacyDTD; +%tabsDTD; +%searchDTD; +%syncBrandDTD; +%syncDTD; +%securityDTD; +%containersDTD; +%sanitizeDTD; +%mainDTD; +%aboutHomeDTD; +%contentDTD; +%applicationsDTD; +%advancedDTD; +]> + +#ifdef XP_WIN +#define USE_WIN_TITLE_STYLE +#endif + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + disablefastfind="true" +#ifdef USE_WIN_TITLE_STYLE + title="&prefWindow.titleWin;"> +#else + title="&prefWindow.title;"> +#endif + + <html:link rel="shortcut icon" + href="chrome://browser/skin/preferences/in-content/favicon.ico"/> + + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/preferences/in-content/preferences.js"/> + <script src="chrome://browser/content/preferences/in-content/subdialogs.js"/> + + <stringbundle id="bundleBrand" + src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundlePreferences" + src="chrome://browser/locale/preferences/preferences.properties"/> + + <stringbundleset id="appManagerBundleset"> + <stringbundle id="appManagerBundle" + src="chrome://browser/locale/preferences/applicationManager.properties"/> + </stringbundleset> + + <stack flex="1"> + <hbox flex="1"> + + <!-- category list --> + <richlistbox id="categories"> + <richlistitem id="category-general" + class="category" + value="paneGeneral" + helpTopic="prefs-main" + tooltiptext="&paneGeneral.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&paneGeneral.title;</label> + </richlistitem> + + <richlistitem id="category-search" + class="category" + value="paneSearch" + helpTopic="prefs-search" + tooltiptext="&paneSearch.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&paneSearch.title;</label> + </richlistitem> + + <richlistitem id="category-content" + class="category" + value="paneContent" + helpTopic="prefs-content" + tooltiptext="&paneContent.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&paneContent.title;</label> + </richlistitem> + + <richlistitem id="category-application" + class="category" + value="paneApplications" + helpTopic="prefs-applications" + tooltiptext="&paneApplications.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&paneApplications.title;</label> + </richlistitem> + + <richlistitem id="category-privacy" + class="category" + value="panePrivacy" + helpTopic="prefs-privacy" + tooltiptext="&panePrivacy.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&panePrivacy.title;</label> + </richlistitem> + + <richlistitem id="category-containers" + class="category" + value="paneContainers" + helpTopic="prefs-containers" + hidden="true"/> + + <richlistitem id="category-security" + class="category" + value="paneSecurity" + helpTopic="prefs-security" + tooltiptext="&paneSecurity.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&paneSecurity.title;</label> + </richlistitem> + + <richlistitem id="category-sync" + class="category" + value="paneSync" + helpTopic="prefs-weave" + tooltiptext="&paneSync.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&paneSync.title;</label> + </richlistitem> + + <richlistitem id="category-advanced" + class="category" + value="paneAdvanced" + helpTopic="prefs-advanced-general" + tooltiptext="&paneAdvanced.title;" + align="center"> + <image class="category-icon"/> + <label class="category-name" flex="1">&paneAdvanced.title;</label> + </richlistitem> + </richlistbox> + + <keyset> + <!-- Disable the findbar because it doesn't work properly. + Remove this keyset once bug 1094240 ("disablefastfind" attribute + broken in e10s mode) is fixed. --> + <key key="&focusSearch1.key;" modifiers="accel" id="focusSearch1" oncommand=";"/> + </keyset> + + <vbox class="main-content" flex="1"> + <prefpane id="mainPrefPane"> +#include main.xul +#include search.xul +#include privacy.xul +#include containers.xul +#include advanced.xul +#include applications.xul +#include content.xul +#include security.xul +#include sync.xul + </prefpane> + </vbox> + + </hbox> + + <vbox id="dialogOverlay" align="center" pack="center"> + <groupbox id="dialogBox" + orient="vertical" + pack="end" + role="dialog" + aria-labelledby="dialogTitle"> + <caption flex="1" align="center"> + <label id="dialogTitle" flex="1"></label> + <button id="dialogClose" + class="close-icon" + aria-label="&preferencesCloseButton.label;"/> + </caption> + <browser id="dialogFrame" + name="dialogFrame" + autoscroll="false" + disablehistory="true"/> + </groupbox> + </vbox> + </stack> +</page> diff --git a/browser/components/preferences/in-content/privacy.js b/browser/components/preferences/in-content/privacy.js new file mode 100644 index 000000000..7dfc7de5a --- /dev/null +++ b/browser/components/preferences/in-content/privacy.js @@ -0,0 +1,712 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/AppConstants.jsm"); +Components.utils.import("resource://gre/modules/PluralForm.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", + "resource://gre/modules/ContextualIdentityService.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +var gPrivacyPane = { + + /** + * Whether the use has selected the auto-start private browsing mode in the UI. + */ + _autoStartPrivateBrowsing: false, + + /** + * Whether the prompt to restart Firefox should appear when changing the autostart pref. + */ + _shouldPromptForRestart: true, + + /** + * Show the Tracking Protection UI depending on the + * privacy.trackingprotection.ui.enabled pref, and linkify its Learn More link + */ + _initTrackingProtection: function () { + if (!Services.prefs.getBoolPref("privacy.trackingprotection.ui.enabled")) { + return; + } + + let link = document.getElementById("trackingProtectionLearnMore"); + let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "tracking-protection"; + link.setAttribute("href", url); + + this.trackingProtectionReadPrefs(); + + document.getElementById("trackingprotectionbox").hidden = false; + document.getElementById("trackingprotectionpbmbox").hidden = true; + }, + + /** + * Linkify the Learn More link of the Private Browsing Mode Tracking + * Protection UI. + */ + _initTrackingProtectionPBM: function () { + let link = document.getElementById("trackingProtectionPBMLearnMore"); + let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "tracking-protection-pbm"; + link.setAttribute("href", url); + }, + + /** + * Initialize autocomplete to ensure prefs are in sync. + */ + _initAutocomplete: function () { + Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Components.interfaces.mozIPlacesAutoComplete); + }, + + /** + * Show the Containers UI depending on the privacy.userContext.ui.enabled pref. + */ + _initBrowserContainers: function () { + if (!Services.prefs.getBoolPref("privacy.userContext.ui.enabled")) { + return; + } + + let link = document.getElementById("browserContainersLearnMore"); + link.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "containers"; + + document.getElementById("browserContainersbox").hidden = false; + + document.getElementById("browserContainersCheckbox").checked = + Services.prefs.getBoolPref("privacy.userContext.enabled"); + }, + + _checkBrowserContainers: function(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 bundlePreferences = document.getElementById("bundlePreferences"); + + let title = bundlePreferences.getString("disableContainersAlertTitle"); + let message = PluralForm.get(count, bundlePreferences.getString("disableContainersMsg")) + .replace("#S", count) + let okButton = PluralForm.get(count, bundlePreferences.getString("disableContainersOkButton")) + .replace("#S", count) + let cancelButton = bundlePreferences.getString("disableContainersButton2"); + + 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) { + ContextualIdentityService.closeAllContainerTabs(); + Services.prefs.setBoolPref("privacy.userContext.enabled", false); + return; + } + + checkbox.checked = true; + }, + + /** + * Sets up the UI for the number of days of history to keep, and updates the + * label of the "Clear Now..." button. + */ + init: function () + { + function setEventListener(aId, aEventType, aCallback) + { + document.getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gPrivacyPane)); + } + + this._updateSanitizeSettingsButton(); + this.initializeHistoryMode(); + this.updateHistoryModePane(); + this.updatePrivacyMicroControls(); + this.initAutoStartPrivateBrowsingReverter(); + this._initTrackingProtection(); + this._initTrackingProtectionPBM(); + this._initAutocomplete(); + this._initBrowserContainers(); + + setEventListener("privacy.sanitize.sanitizeOnShutdown", "change", + gPrivacyPane._updateSanitizeSettingsButton); + setEventListener("browser.privatebrowsing.autostart", "change", + gPrivacyPane.updatePrivacyMicroControls); + setEventListener("historyMode", "command", function () { + gPrivacyPane.updateHistoryModePane(); + gPrivacyPane.updateHistoryModePrefs(); + gPrivacyPane.updatePrivacyMicroControls(); + gPrivacyPane.updateAutostart(); + }); + setEventListener("historyRememberClear", "click", function () { + gPrivacyPane.clearPrivateDataNow(false); + return false; + }); + setEventListener("historyRememberCookies", "click", function () { + gPrivacyPane.showCookies(); + return false; + }); + setEventListener("historyDontRememberClear", "click", function () { + gPrivacyPane.clearPrivateDataNow(true); + return false; + }); + setEventListener("doNotTrackSettings", "click", function () { + gPrivacyPane.showDoNotTrackSettings(); + return false; + }); + setEventListener("privateBrowsingAutoStart", "command", + gPrivacyPane.updateAutostart); + setEventListener("cookieExceptions", "command", + gPrivacyPane.showCookieExceptions); + setEventListener("showCookiesButton", "command", + gPrivacyPane.showCookies); + setEventListener("clearDataSettings", "command", + gPrivacyPane.showClearPrivateDataSettings); + setEventListener("trackingProtectionRadioGroup", "command", + gPrivacyPane.trackingProtectionWritePrefs); + setEventListener("trackingProtectionExceptions", "command", + gPrivacyPane.showTrackingProtectionExceptions); + setEventListener("changeBlockList", "command", + gPrivacyPane.showBlockLists); + setEventListener("changeBlockListPBM", "command", + gPrivacyPane.showBlockLists); + setEventListener("browserContainersCheckbox", "command", + gPrivacyPane._checkBrowserContainers); + setEventListener("browserContainersSettings", "command", + gPrivacyPane.showContainerSettings); + }, + + // TRACKING PROTECTION MODE + + /** + * Selects the right item of the Tracking Protection radiogroup. + */ + trackingProtectionReadPrefs() { + let enabledPref = document.getElementById("privacy.trackingprotection.enabled"); + let pbmPref = document.getElementById("privacy.trackingprotection.pbmode.enabled"); + let radiogroup = document.getElementById("trackingProtectionRadioGroup"); + + // Global enable takes precedence over enabled in Private Browsing. + if (enabledPref.value) { + radiogroup.value = "always"; + } else if (pbmPref.value) { + radiogroup.value = "private"; + } else { + radiogroup.value = "never"; + } + }, + + /** + * Sets the pref values based on the selected item of the radiogroup. + */ + trackingProtectionWritePrefs() { + let enabledPref = document.getElementById("privacy.trackingprotection.enabled"); + let pbmPref = document.getElementById("privacy.trackingprotection.pbmode.enabled"); + let radiogroup = document.getElementById("trackingProtectionRadioGroup"); + + switch (radiogroup.value) { + case "always": + enabledPref.value = true; + pbmPref.value = true; + break; + case "private": + enabledPref.value = false; + pbmPref.value = true; + break; + case "never": + enabledPref.value = false; + pbmPref.value = false; + break; + } + }, + + // 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 + "network.cookie.cookieBehavior": 0, // All cookies are enabled + "network.cookie.lifetimePolicy": 0, // Cookies use supplied lifetime + "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", + "keepUntil", + "keepCookiesUntil", + "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: function(aPrefs) { + for (let pref of Object.keys(aPrefs)) { + if (document.getElementById(pref).value != aPrefs[pref]) + return false; + } + return true; + }, + + /** + * Initialize the history mode menulist based on the privacy preferences + */ + initializeHistoryMode: function PPP_initializeHistoryMode() + { + let mode; + let getVal = aPref => document.getElementById(aPref).value; + + 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: function PPP_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; + }, + + /** + * Update the private browsing auto-start pref and the history mode + * micro-management prefs based on the history mode menulist + */ + updateHistoryModePrefs: function PPP_updateHistoryModePrefs() + { + let pref = document.getElementById("browser.privatebrowsing.autostart"); + switch (document.getElementById("historyMode").value) { + case "remember": + if (pref.value) + pref.value = false; + + // select the remember history option if needed + let rememberHistoryCheckbox = document.getElementById("rememberHistory"); + if (!rememberHistoryCheckbox.checked) + rememberHistoryCheckbox.checked = true; + + // select the remember forms history option + document.getElementById("browser.formfill.enable").value = true; + + // select the allow cookies option + document.getElementById("network.cookie.cookieBehavior").value = 0; + // select the cookie lifetime policy option + document.getElementById("network.cookie.lifetimePolicy").value = 0; + + // select the clear on close option + document.getElementById("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 checkbox. + */ + updatePrivacyMicroControls: function PPP_updatePrivacyMicroControls() + { + if (document.getElementById("historyMode").value == "custom") { + let disabled = this._autoStartPrivateBrowsing = + document.getElementById("privateBrowsingAutoStart").checked; + this.dependentControls.forEach(function (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 ? document.getElementById(preferenceId) : {}; + control.disabled = disabled || preference.locked; + }); + + // adjust the cookie controls status + this.readAcceptCookies(); + let lifetimePolicy = document.getElementById("network.cookie.lifetimePolicy").value; + if (lifetimePolicy != Ci.nsICookieService.ACCEPT_NORMALLY && + lifetimePolicy != Ci.nsICookieService.ACCEPT_SESSION && + lifetimePolicy != Ci.nsICookieService.ACCEPT_FOR_N_DAYS) { + lifetimePolicy = Ci.nsICookieService.ACCEPT_NORMALLY; + } + document.getElementById("keepCookiesUntil").value = disabled ? 2 : lifetimePolicy; + + // adjust the checked state of the sanitizeOnShutdown checkbox + document.getElementById("alwaysClear").checked = disabled ? false : + document.getElementById("privacy.sanitize.sanitizeOnShutdown").value; + + // adjust the checked state of the remember history checkboxes + document.getElementById("rememberHistory").checked = disabled ? false : + document.getElementById("places.history.enabled").value; + document.getElementById("rememberForms").checked = disabled ? false : + document.getElementById("browser.formfill.enable").value; + + if (!disabled) { + // adjust the Settings button for sanitizeOnShutdown + this._updateSanitizeSettingsButton(); + } + } + }, + + // PRIVATE BROWSING + + /** + * Initialize the starting state for the auto-start private browsing mode pref reverter. + */ + initAutoStartPrivateBrowsingReverter: function PPP_initAutoStartPrivateBrowsingReverter() + { + let mode = document.getElementById("historyMode"); + let autoStart = document.getElementById("privateBrowsingAutoStart"); + this._lastMode = mode.selectedIndex; + this._lastCheckState = autoStart.hasAttribute('checked'); + }, + + _lastMode: null, + _lastCheckState: null, + updateAutostart: function PPP_updateAutostart() { + let mode = document.getElementById("historyMode"); + let autoStart = document.getElementById("privateBrowsingAutoStart"); + let pref = document.getElementById("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 = confirmRestartPrompt(autoStart.checked, 1, + true, false); + if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) { + pref.value = autoStart.hasAttribute('checked'); + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.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 bundlePreferences = document.getElementById("bundlePreferences"); + let params = { + permissionType: "trackingprotection", + hideStatusColumn: true, + windowTitle: bundlePreferences.getString("trackingprotectionpermissionstitle"), + introText: bundlePreferences.getString("trackingprotectionpermissionstext"), + }; + gSubDialog.open("chrome://browser/content/preferences/permissions.xul", + null, params); + }, + + /** + * Displays container panel for customising and adding containers. + */ + showContainerSettings() { + gotoPref("containers"); + }, + + /** + * Displays the available block lists for tracking protection. + */ + showBlockLists: function () + { + var bundlePreferences = document.getElementById("bundlePreferences"); + let brandName = document.getElementById("bundleBrand") + .getString("brandShortName"); + var params = { brandShortName: brandName, + windowTitle: bundlePreferences.getString("blockliststitle"), + introText: bundlePreferences.getString("blockliststext") }; + gSubDialog.open("chrome://browser/content/preferences/blocklists.xul", + null, params); + }, + + /** + * Displays the Do Not Track settings dialog. + */ + showDoNotTrackSettings() { + gSubDialog.open("chrome://browser/content/preferences/donottrack.xul", + "resizable=no"); + }, + + // HISTORY + + /* + * Preferences: + * + * places.history.enabled + * - whether history is enabled or not + * browser.formfill.enable + * - true if entries in forms and the search bar should be saved, false + * otherwise + */ + + // COOKIES + + /* + * 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 + * see netwerk/cookie/src/nsCookieService.cpp for details + * network.cookie.lifetimePolicy + * - determines how long cookies are stored: + * 0 means keep cookies until they expire + * 2 means keep cookies until the browser is closed + */ + + /** + * Reads the network.cookie.cookieBehavior preference value and + * enables/disables the rest of the cookie UI accordingly, returning true + * if cookies are enabled. + */ + readAcceptCookies: function () + { + var pref = document.getElementById("network.cookie.cookieBehavior"); + var acceptThirdPartyLabel = document.getElementById("acceptThirdPartyLabel"); + var acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu"); + var keepUntil = document.getElementById("keepUntil"); + var menu = document.getElementById("keepCookiesUntil"); + + // enable the rest of the UI for anything other than "disable all cookies" + var acceptCookies = (pref.value != 2); + + acceptThirdPartyLabel.disabled = acceptThirdPartyMenu.disabled = !acceptCookies; + keepUntil.disabled = menu.disabled = this._autoStartPrivateBrowsing || !acceptCookies; + + return acceptCookies; + }, + + /** + * Enables/disables the "keep until" label and menulist in response to the + * "accept cookies" checkbox being checked or unchecked. + */ + writeAcceptCookies: function () + { + var accept = document.getElementById("acceptCookies"); + var acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu"); + + // if we're enabling cookies, automatically select 'accept third party always' + if (accept.checked) + acceptThirdPartyMenu.selectedIndex = 0; + + return accept.checked ? 0 : 2; + }, + + /** + * Converts between network.cookie.cookieBehavior and the third-party cookie UI + */ + readAcceptThirdPartyCookies: function () + { + var pref = document.getElementById("network.cookie.cookieBehavior"); + switch (pref.value) + { + case 0: + return "always"; + case 1: + return "never"; + case 2: + return "never"; + case 3: + return "visited"; + default: + return undefined; + } + }, + + writeAcceptThirdPartyCookies: function () + { + var accept = document.getElementById("acceptThirdPartyMenu").selectedItem; + switch (accept.value) + { + case "always": + return 0; + case "visited": + return 3; + case "never": + return 1; + default: + return undefined; + } + }, + + /** + * Displays fine-grained, per-site preferences for cookies. + */ + showCookieExceptions: function () + { + var bundlePreferences = document.getElementById("bundlePreferences"); + var params = { blockVisible : true, + sessionVisible : true, + allowVisible : true, + prefilledHost : "", + permissionType : "cookie", + windowTitle : bundlePreferences.getString("cookiepermissionstitle"), + introText : bundlePreferences.getString("cookiepermissionstext") }; + gSubDialog.open("chrome://browser/content/preferences/permissions.xul", + null, params); + }, + + /** + * Displays all the user's cookies in a dialog. + */ + showCookies: function (aCategory) + { + gSubDialog.open("chrome://browser/content/preferences/cookies.xul"); + }, + + // 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: function () + { + gSubDialog.open("chrome://browser/content/preferences/sanitize.xul", "resizable=no"); + }, + + + /** + * Displays a dialog from which individual parts of private data may be + * cleared. + */ + clearPrivateDataNow: function (aClearEverything) { + var ts = document.getElementById("privacy.sanitize.timeSpan"); + var timeSpanOrig = ts.value; + + if (aClearEverything) { + ts.value = 0; + } + + gSubDialog.open("chrome://browser/content/sanitize.xul", "resizable=no", null, () => { + // reset the timeSpan pref + if (aClearEverything) { + ts.value = timeSpanOrig; + } + + Services.obs.notifyObservers(null, "clear-private-data", null); + }); + }, + + /** + * Enables or disables the "Settings..." button depending + * on the privacy.sanitize.sanitizeOnShutdown preference value + */ + _updateSanitizeSettingsButton: function () { + var settingsButton = document.getElementById("clearDataSettings"); + var sanitizeOnShutdownPref = document.getElementById("privacy.sanitize.sanitizeOnShutdown"); + + settingsButton.disabled = !sanitizeOnShutdownPref.value; + }, + + // CONTAINERS + + /* + * preferences: + * + * privacy.userContext.enabled + * - true if containers is enabled + */ + + /** + * Enables/disables the Settings button used to configure containers + */ + readBrowserContainersCheckbox: function () + { + var pref = document.getElementById("privacy.userContext.enabled"); + var settings = document.getElementById("browserContainersSettings"); + + settings.disabled = !pref.value; + } + +}; diff --git a/browser/components/preferences/in-content/privacy.xul b/browser/components/preferences/in-content/privacy.xul new file mode 100644 index 000000000..6ac6c88a4 --- /dev/null +++ b/browser/components/preferences/in-content/privacy.xul @@ -0,0 +1,308 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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 type="application/javascript" + src="chrome://browser/content/preferences/in-content/privacy.js"/> + +<preferences id="privacyPreferences" hidden="true" data-category="panePrivacy"> + + <!-- Tracking --> + <preference id="privacy.trackingprotection.enabled" + name="privacy.trackingprotection.enabled" + type="bool"/> + <preference id="privacy.trackingprotection.pbmode.enabled" + name="privacy.trackingprotection.pbmode.enabled" + type="bool"/> + + <!-- XXX button prefs --> + <preference id="pref.privacy.disable_button.cookie_exceptions" + name="pref.privacy.disable_button.cookie_exceptions" + type="bool"/> + <preference id="pref.privacy.disable_button.view_cookies" + name="pref.privacy.disable_button.view_cookies" + type="bool"/> + <preference id="pref.privacy.disable_button.change_blocklist" + name="pref.privacy.disable_button.change_blocklist" + type="bool"/> + <preference id="pref.privacy.disable_button.tracking_protection_exceptions" + name="pref.privacy.disable_button.tracking_protection_exceptions" + type="bool"/> + + <!-- Location Bar --> + <preference id="browser.urlbar.autocomplete.enabled" + name="browser.urlbar.autocomplete.enabled" + type="bool"/> + <preference id="browser.urlbar.suggest.bookmark" + name="browser.urlbar.suggest.bookmark" + type="bool"/> + <preference id="browser.urlbar.suggest.history" + name="browser.urlbar.suggest.history" + type="bool"/> + <preference id="browser.urlbar.suggest.openpage" + name="browser.urlbar.suggest.openpage" + type="bool"/> + + <!-- History --> + <preference id="places.history.enabled" + name="places.history.enabled" + type="bool"/> + <preference id="browser.formfill.enable" + name="browser.formfill.enable" + type="bool"/> + <!-- Cookies --> + <preference id="network.cookie.cookieBehavior" + name="network.cookie.cookieBehavior" + type="int"/> + <preference id="network.cookie.lifetimePolicy" + name="network.cookie.lifetimePolicy" + type="int"/> + <preference id="network.cookie.blockFutureCookies" + name="network.cookie.blockFutureCookies" + type="bool"/> + <!-- Clear Private Data --> + <preference id="privacy.sanitize.sanitizeOnShutdown" + name="privacy.sanitize.sanitizeOnShutdown" + type="bool"/> + <preference id="privacy.sanitize.timeSpan" + name="privacy.sanitize.timeSpan" + type="int"/> + <!-- Private Browsing --> + <preference id="browser.privatebrowsing.autostart" + name="browser.privatebrowsing.autostart" + type="bool"/> +</preferences> + +<hbox id="header-privacy" + class="header" + hidden="true" + data-category="panePrivacy"> + <label class="header-name" flex="1">&panePrivacy.title;</label> + <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a> +</hbox> + +<!-- Tracking --> +<groupbox id="trackingGroup" data-category="panePrivacy" hidden="true"> + <vbox id="trackingprotectionbox" hidden="true"> + <hbox align="start"> + <vbox> + <caption><label>&trackingProtectionHeader.label; + <label id="trackingProtectionLearnMore" class="text-link" + value="&trackingProtectionLearnMore.label;"/> + </label></caption> + <radiogroup id="trackingProtectionRadioGroup"> + <radio value="always" + label="&trackingProtectionAlways.label;" + accesskey="&trackingProtectionAlways.accesskey;"/> + <radio value="private" + label="&trackingProtectionPrivate.label;" + accesskey="&trackingProtectionPrivate.accesskey;"/> + <radio value="never" + label="&trackingProtectionNever.label;" + accesskey="&trackingProtectionNever.accesskey;"/> + </radiogroup> + </vbox> + <spacer flex="1" /> + <vbox> + <button id="trackingProtectionExceptions" + label="&trackingProtectionExceptions.label;" + accesskey="&trackingProtectionExceptions.accesskey;" + preference="pref.privacy.disable_button.tracking_protection_exceptions"/> + <button id="changeBlockList" + label="&changeBlockList.label;" + accesskey="&changeBlockList.accesskey;" + preference="pref.privacy.disable_button.change_blocklist"/> + </vbox> + </hbox> + </vbox> + <vbox id="trackingprotectionpbmbox"> + <caption><label>&tracking.label;</label></caption> + <hbox align="center"> + <checkbox id="trackingProtectionPBM" + preference="privacy.trackingprotection.pbmode.enabled" + accesskey="&trackingProtectionPBM5.accesskey;" + label="&trackingProtectionPBM5.label;" /> + <label id="trackingProtectionPBMLearnMore" + class="text-link" + value="&trackingProtectionPBMLearnMore.label;"/> + <spacer flex="1" /> + <button id="changeBlockListPBM" + label="&changeBlockList.label;" accesskey="&changeBlockList.accesskey;" + preference="pref.privacy.disable_button.change_blocklist"/> + </hbox> + </vbox> + <vbox> + <description>&doNotTrack.pre.label;<label + class="text-link" id="doNotTrackSettings" + >&doNotTrack.settings.label;</label>&doNotTrack.post.label;</description> + </vbox> +</groupbox> + +<!-- History --> +<groupbox id="historyGroup" data-category="panePrivacy" hidden="true"> + <caption><label>&history.label;</label></caption> + <hbox align="center"> + <label id="historyModeLabel" + control="historyMode" + accesskey="&historyHeader.pre.accesskey;">&historyHeader.pre.label; + </label> + <menulist id="historyMode"> + <menupopup> + <menuitem label="&historyHeader.remember.label;" value="remember"/> + <menuitem label="&historyHeader.dontremember.label;" value="dontremember"/> + <menuitem label="&historyHeader.custom.label;" value="custom"/> + </menupopup> + </menulist> + <label>&historyHeader.post.label;</label> + </hbox> + <deck id="historyPane"> + <vbox id="historyRememberPane"> + <hbox align="center" flex="1"> + <vbox flex="1"> + <description>&rememberDescription.label;</description> + <separator class="thin"/> + <description>&rememberActions.pre.label;<label + class="text-link" id="historyRememberClear" + >&rememberActions.clearHistory.label;</label>&rememberActions.middle.label;<label + class="text-link" id="historyRememberCookies" + >&rememberActions.removeCookies.label;</label>&rememberActions.post.label;</description> + </vbox> + </hbox> + </vbox> + <vbox id="historyDontRememberPane"> + <hbox align="center" flex="1"> + <vbox flex="1"> + <description>&dontrememberDescription.label;</description> + <separator class="thin"/> + <description>&dontrememberActions.pre.label;<label + class="text-link" id="historyDontRememberClear" + >&dontrememberActions.clearHistory.label;</label>&dontrememberActions.post.label;</description> + </vbox> + </hbox> + </vbox> + <vbox id="historyCustomPane"> + <separator class="thin"/> + <vbox> + <vbox align="start"> + <checkbox id="privateBrowsingAutoStart" + label="&privateBrowsingPermanent2.label;" + accesskey="&privateBrowsingPermanent2.accesskey;" + preference="browser.privatebrowsing.autostart"/> + </vbox> + <vbox class="indent"> + <vbox align="start"> + <checkbox id="rememberHistory" + label="&rememberHistory2.label;" + accesskey="&rememberHistory2.accesskey;" + preference="places.history.enabled"/> + <checkbox id="rememberForms" + label="&rememberSearchForm.label;" + accesskey="&rememberSearchForm.accesskey;" + preference="browser.formfill.enable"/> + </vbox> + <hbox id="cookiesBox"> + <checkbox id="acceptCookies" label="&acceptCookies.label;" + preference="network.cookie.cookieBehavior" + accesskey="&acceptCookies.accesskey;" + onsyncfrompreference="return gPrivacyPane.readAcceptCookies();" + onsynctopreference="return gPrivacyPane.writeAcceptCookies();"/> + <spacer flex="1" /> + <button id="cookieExceptions" + label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;" + preference="pref.privacy.disable_button.cookie_exceptions"/> + </hbox> + <hbox id="acceptThirdPartyRow" + class="indent" + align="center"> + <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu" + accesskey="&acceptThirdParty.pre.accesskey;">&acceptThirdParty.pre.label;</label> + <menulist id="acceptThirdPartyMenu" preference="network.cookie.cookieBehavior" + onsyncfrompreference="return gPrivacyPane.readAcceptThirdPartyCookies();" + onsynctopreference="return gPrivacyPane.writeAcceptThirdPartyCookies();"> + <menupopup> + <menuitem label="&acceptThirdParty.always.label;" value="always"/> + <menuitem label="&acceptThirdParty.visited.label;" value="visited"/> + <menuitem label="&acceptThirdParty.never.label;" value="never"/> + </menupopup> + </menulist> + </hbox> + <hbox id="keepRow" + class="indent" + align="center"> + <label id="keepUntil" + control="keepCookiesUntil" + accesskey="&keepUntil.accesskey;">&keepUntil.label;</label> + <menulist id="keepCookiesUntil" + preference="network.cookie.lifetimePolicy"> + <menupopup> + <menuitem label="&expire.label;" value="0"/> + <menuitem label="&close.label;" value="2"/> + </menupopup> + </menulist> + <spacer flex="1"/> + <button id="showCookiesButton" + label="&showCookies.label;" accesskey="&showCookies.accesskey;" + preference="pref.privacy.disable_button.view_cookies"/> + </hbox> + <hbox id="clearDataBox" + align="center"> + <checkbox id="alwaysClear" + preference="privacy.sanitize.sanitizeOnShutdown" + label="&clearOnClose.label;" + accesskey="&clearOnClose.accesskey;"/> + <spacer flex="1"/> + <button id="clearDataSettings" label="&clearOnCloseSettings.label;" + accesskey="&clearOnCloseSettings.accesskey;"/> + </hbox> + </vbox> + </vbox> + </vbox> + </deck> +</groupbox> + +<!-- Location Bar --> +<groupbox id="locationBarGroup" + data-category="panePrivacy" + hidden="true"> + <caption><label>&locationBar.label;</label></caption> + <label id="locationBarSuggestionLabel">&locbar.suggest.label;</label> + <checkbox id="historySuggestion" label="&locbar.history.label;" + accesskey="&locbar.history.accesskey;" + preference="browser.urlbar.suggest.history"/> + <checkbox id="bookmarkSuggestion" label="&locbar.bookmarks.label;" + accesskey="&locbar.bookmarks.accesskey;" + preference="browser.urlbar.suggest.bookmark"/> + <checkbox id="openpageSuggestion" label="&locbar.openpage.label;" + accesskey="&locbar.openpage.accesskey;" + preference="browser.urlbar.suggest.openpage"/> + <label class="text-link" onclick="gotoPref('search')"> + &suggestionSettings.label; + </label> +</groupbox> + +<!-- Containers --> +<groupbox id="browserContainersGroup" data-category="panePrivacy" hidden="true"> + <vbox id="browserContainersbox" hidden="true"> + <caption><label>&browserContainersHeader.label; + <label id="browserContainersLearnMore" class="text-link" + value="&browserContainersLearnMore.label;"/> + </label></caption> + <hbox align="start"> + <vbox> + <checkbox id="browserContainersCheckbox" + label="&browserContainersEnabled.label;" + accesskey="&browserContainersEnabled.accesskey;" + preference="privacy.userContext.enabled" + onsyncfrompreference="return gPrivacyPane.readBrowserContainersCheckbox();"/> + </vbox> + <spacer flex="1"/> + <vbox> + <button id="browserContainersSettings" + label="&browserContainersSettings.label;" + accesskey="&browserContainersSettings.accesskey;"/> + </vbox> + </hbox> + </vbox> +</groupbox> diff --git a/browser/components/preferences/in-content/search.js b/browser/components/preferences/in-content/search.js new file mode 100644 index 000000000..55aa2c18c --- /dev/null +++ b/browser/components/preferences/in-content/search.js @@ -0,0 +1,604 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const ENGINE_FLAVOR = "text/x-moz-search-engine"; + +var gEngineView = null; + +var gSearchPane = { + + /** + * Initialize autocomplete to ensure prefs are in sync. + */ + _initAutocomplete: function () { + Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Components.interfaces.mozIPlacesAutoComplete); + }, + + init: function () + { + gEngineView = new EngineView(new EngineStore()); + document.getElementById("engineList").view = gEngineView; + this.buildDefaultEngineDropDown(); + + let addEnginesLink = document.getElementById("addEngines"); + let searchEnginesURL = Services.wm.getMostRecentWindow('navigator:browser') + .BrowserSearch.searchEnginesURL; + addEnginesLink.setAttribute("href", searchEnginesURL); + + window.addEventListener("click", this, false); + window.addEventListener("command", this, false); + window.addEventListener("dragstart", this, false); + window.addEventListener("keypress", this, false); + window.addEventListener("select", this, false); + window.addEventListener("blur", this, true); + + Services.obs.addObserver(this, "browser-search-engine-modified", false); + window.addEventListener("unload", () => { + Services.obs.removeObserver(this, "browser-search-engine-modified", false); + }); + + this._initAutocomplete(); + + let suggestsPref = + document.getElementById("browser.search.suggest.enabled"); + suggestsPref.addEventListener("change", () => { + this.updateSuggestsCheckbox(); + }); + this.updateSuggestsCheckbox(); + }, + + updateSuggestsCheckbox() { + let suggestsPref = + document.getElementById("browser.search.suggest.enabled"); + let permanentPB = + Services.prefs.getBoolPref("browser.privatebrowsing.autostart"); + let urlbarSuggests = document.getElementById("urlBarSuggestion"); + urlbarSuggests.disabled = !suggestsPref.value || permanentPB; + + let urlbarSuggestsPref = + document.getElementById("browser.urlbar.suggest.searches"); + urlbarSuggests.checked = urlbarSuggestsPref.value; + if (urlbarSuggests.disabled) { + urlbarSuggests.checked = false; + } + + let permanentPBLabel = + document.getElementById("urlBarSuggestionPermanentPBLabel"); + permanentPBLabel.hidden = urlbarSuggests.hidden || !permanentPB; + }, + + buildDefaultEngineDropDown: function() { + // This is called each time something affects the list of engines. + let list = document.getElementById("defaultEngine"); + // Set selection to the current default engine. + let currentEngine = Services.search.currentEngine.name; + + // If the current engine isn't in the list any more, select the first item. + let engines = gEngineView._engineStore._engines; + 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: function(aEvent) { + switch (aEvent.type) { + 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 && + aEvent.target.parentNode.parentNode.id == "defaultEngine") { + gSearchPane.setDefaultEngine(); + } + break; + case "restoreDefaultSearchEngines": + gSearchPane.onRestoreDefaults(); + break; + case "removeEngineButton": + Services.search.removeEngine(gEngineView.selectedEngine.originalEngine); + 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; + case "blur": + if (aEvent.target.id == "engineList" && + aEvent.target.inputField == document.getBindingParent(aEvent.originalTarget)) { + gSearchPane.onInputBlur(); + } + break; + } + }, + + observe: function(aEngine, aTopic, aVerb) { + if (aTopic == "browser-search-engine-modified") { + aEngine.QueryInterface(Components.interfaces.nsISearchEngine); + switch (aVerb) { + case "engine-added": + gEngineView._engineStore.addEngine(aEngine); + gEngineView.rowCountChanged(gEngineView.lastIndex, 1); + gSearchPane.buildDefaultEngineDropDown(); + break; + case "engine-changed": + gEngineView._engineStore.reloadIcons(); + gEngineView.invalidate(); + break; + case "engine-removed": + gSearchPane.remove(aEngine); + break; + case "engine-current": + // If the user is going through the drop down using up/down keys, the + // dropdown may still be open (eg. on Windows) when engine-current is + // fired, so rebuilding the list unconditionally would get in the way. + let selectedEngine = + document.getElementById("defaultEngine").selectedItem.engine; + if (selectedEngine.name != aEngine.name) + gSearchPane.buildDefaultEngineDropDown(); + break; + case "engine-default": + // Not relevant + break; + } + } + }, + + onInputBlur: function(aEvent) { + let tree = document.getElementById("engineList"); + if (!tree.hasAttribute("editing")) + return; + + // Accept input unless discarded. + let accept = aEvent.charCode != KeyEvent.DOM_VK_ESCAPE; + tree.stopEditing(accept); + }, + + onTreeSelect: function() { + document.getElementById("removeEngineButton").disabled = + !gEngineView.isEngineSelectedAndRemovable(); + }, + + onTreeKeyPress: function(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._engineStore.engines[index].shown; + 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)) { + tree.startEditing(index, tree.columns.getLastColumn()); + } 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); + } + } + }, + + onRestoreDefaults: function() { + let num = gEngineView._engineStore.restoreDefaultEngines(); + gEngineView.rowCountChanged(0, num); + gEngineView.invalidate(); + }, + + showRestoreDefaults: function(aEnable) { + document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable; + }, + + remove: function(aEngine) { + let index = gEngineView._engineStore.removeEngine(aEngine); + gEngineView.rowCountChanged(index, -1); + gEngineView.invalidate(); + gEngineView.selection.select(Math.min(index, gEngineView.lastIndex)); + gEngineView.ensureRowIsVisible(gEngineView.currentIndex); + document.getElementById("engineList").focus(); + }, + + editKeyword: Task.async(function* (aEngine, aNewKeyword) { + let keyword = aNewKeyword.trim(); + if (keyword) { + let eduplicate = false; + let dupName = ""; + + // Check for duplicates in Places keywords. + let bduplicate = !!(yield PlacesUtils.keywords.fetch(keyword)); + + // Check for duplicates in changes we haven't committed yet + let engines = gEngineView._engineStore.engines; + for (let engine of engines) { + if (engine.alias == 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 strings = document.getElementById("engineManagerBundle"); + let dtitle = strings.getString("duplicateTitle"); + let bmsg = strings.getString("duplicateBookmarkMsg"); + let emsg = strings.getFormattedString("duplicateEngineMsg", [dupName]); + + Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg); + return false; + } + } + + gEngineView._engineStore.changeEngine(aEngine, "alias", keyword); + gEngineView.invalidate(); + return true; + }), + + saveOneClickEnginesList: function () { + let hiddenList = []; + for (let engine of gEngineView._engineStore.engines) { + if (!engine.shown) + hiddenList.push(engine.name); + } + document.getElementById("browser.search.hiddenOneOffs").value = + hiddenList.join(","); + }, + + setDefaultEngine: function () { + Services.search.currentEngine = + document.getElementById("defaultEngine").selectedItem.engine; + } +}; + +function onDragEngineStart(event) { + var selectedIndex = gEngineView.selectedIndex; + var tree = document.getElementById("engineList"); + var row = { }, col = { }, child = { }; + tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child); + if (selectedIndex >= 0 && !gEngineView.isCheckBox(row.value, col.value)) { + event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); + event.dataTransfer.effectAllowed = "move"; + } +} + + +function EngineStore() { + let pref = document.getElementById("browser.search.hiddenOneOffs").value; + this.hiddenList = pref ? pref.split(",") : []; + + this._engines = Services.search.getVisibleEngines().map(this._cloneEngine, this); + this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine, this); + + // check if we need to disable the restore defaults button + var someHidden = this._defaultEngines.some(e => e.hidden); + gSearchPane.showRestoreDefaults(someHidden); +} +EngineStore.prototype = { + _engines: null, + _defaultEngines: null, + + get engines() { + return this._engines; + }, + set engines(val) { + this._engines = val; + return val; + }, + + _getIndexForEngine: function ES_getIndexForEngine(aEngine) { + return this._engines.indexOf(aEngine); + }, + + _getEngineByName: function ES_getEngineByName(aName) { + return this._engines.find(engine => engine.name == aName); + }, + + _cloneEngine: function ES_cloneEngine(aEngine) { + var clonedObj={}; + for (var i in aEngine) + clonedObj[i] = aEngine[i]; + clonedObj.originalEngine = aEngine; + clonedObj.shown = this.hiddenList.indexOf(clonedObj.name) == -1; + return clonedObj; + }, + + // Callback for Array's some(). A thisObj must be passed to some() + _isSameEngine: function ES_isSameEngine(aEngineClone) { + return aEngineClone.originalEngine == this.originalEngine; + }, + + addEngine: function ES_addEngine(aEngine) { + this._engines.push(this._cloneEngine(aEngine)); + }, + + moveEngine: function ES_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; // nothing to do + + // Move the engine in our internal store + var removedEngine = this._engines.splice(index, 1)[0]; + this._engines.splice(aNewIndex, 0, removedEngine); + + Services.search.moveEngine(aEngine.originalEngine, aNewIndex); + }, + + removeEngine: function ES_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?"); + + let removedEngine = this._engines.splice(index, 1)[0]; + + if (this._defaultEngines.some(this._isSameEngine, removedEngine)) + gSearchPane.showRestoreDefaults(true); + gSearchPane.buildDefaultEngineDropDown(); + return index; + }, + + restoreDefaultEngines: function ES_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)) { + 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; + Services.search.moveEngine(engine, i); + added++; + } + } + Services.search.resetToOriginalDefaultEngine(); + gSearchPane.showRestoreDefaults(false); + gSearchPane.buildDefaultEngineDropDown(); + return added; + }, + + changeEngine: function ES_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: function ES_reloadIcons() { + this._engines.forEach(function (e) { + e.uri = e.originalEngine.uri; + }); + } +}; + +function EngineView(aEngineStore) { + this._engineStore = aEngineStore; +} +EngineView.prototype = { + _engineStore: null, + tree: null, + + get lastIndex() { + return this.rowCount - 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: function (index, count) { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function () { + this.tree.invalidate(); + }, + + ensureRowIsVisible: function (index) { + this.tree.ensureRowIsVisible(index); + }, + + getSourceIndexFromDrag: function (dataTransfer) { + return parseInt(dataTransfer.getData(ENGINE_FLAVOR)); + }, + + isCheckBox: function(index, column) { + return column.id == "engineShown"; + }, + + isEngineSelectedAndRemovable: function() { + return this.selectedIndex != -1 && this.lastIndex != 0; + }, + + // nsITreeView + get rowCount() { + return this._engineStore.engines.length; + }, + + getImageSrc: function(index, column) { + if (column.id == "engineName") { + 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: function(index, column) { + if (column.id == "engineName") + return this._engineStore.engines[index].name; + else if (column.id == "engineKeyword") + return this._engineStore.engines[index].alias; + return ""; + }, + + setTree: function(tree) { + this.tree = tree; + }, + + canDrop: function(targetIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + return (sourceIndex != -1 && + sourceIndex != targetIndex && + sourceIndex != targetIndex + orientation); + }, + + drop: function(dropIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + var sourceEngine = this._engineStore.engines[sourceIndex]; + + const nsITreeView = Components.interfaces.nsITreeView; + if (dropIndex > sourceIndex) { + if (orientation == nsITreeView.DROP_BEFORE) + dropIndex--; + } else if (orientation == nsITreeView.DROP_AFTER) { + dropIndex++; + } + + this._engineStore.moveEngine(sourceEngine, dropIndex); + gSearchPane.showRestoreDefaults(true); + gSearchPane.buildDefaultEngineDropDown(); + + // Redraw, and adjust selection + this.invalidate(); + this.selection.select(dropIndex); + }, + + selection: null, + getRowProperties: function(index) { return ""; }, + getCellProperties: function(index, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isContainerEmpty: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function(index) { return false; }, + getParentIndex: function(index) { return -1; }, + hasNextSibling: function(parentIndex, index) { return false; }, + getLevel: function(index) { return 0; }, + getProgressMode: function(index, column) { }, + getCellValue: function(index, column) { + if (column.id == "engineShown") + return this._engineStore.engines[index].shown; + return undefined; + }, + toggleOpenState: function(index) { }, + cycleHeader: function(column) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(index, column) { return column.id != "engineName"; }, + isSelectable: function(index, column) { return false; }, + setCellValue: function(index, column, value) { + if (column.id == "engineShown") { + this._engineStore.engines[index].shown = value == "true"; + gEngineView.invalidate(); + gSearchPane.saveOneClickEnginesList(); + } + }, + setCellText: function(index, column, value) { + if (column.id == "engineKeyword") { + gSearchPane.editKeyword(this._engineStore.engines[index], value) + .then(valid => { + if (!valid) + document.getElementById("engineList").startEditing(index, column); + }); + } + }, + performAction: function(action) { }, + performActionOnRow: function(action, index) { }, + performActionOnCell: function(action, index, column) { } +}; diff --git a/browser/components/preferences/in-content/search.xul b/browser/components/preferences/in-content/search.xul new file mode 100644 index 000000000..95c7acd85 --- /dev/null +++ b/browser/components/preferences/in-content/search.xul @@ -0,0 +1,86 @@ + <preferences id="searchPreferences" hidden="true" data-category="paneSearch"> + + <preference id="browser.search.suggest.enabled" + name="browser.search.suggest.enabled" + type="bool"/> + + <preference id="browser.urlbar.suggest.searches" + name="browser.urlbar.suggest.searches" + type="bool"/> + + <preference id="browser.search.hiddenOneOffs" + name="browser.search.hiddenOneOffs" + type="unichar"/> + + </preferences> + + <script type="application/javascript" + src="chrome://browser/content/preferences/in-content/search.js"/> + + <stringbundle id="engineManagerBundle" src="chrome://browser/locale/engineManager.properties"/> + + <hbox id="header-search" + class="header" + hidden="true" + data-category="paneSearch"> + <label class="header-name" flex="1">&paneSearch.title;</label> + <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a> + </hbox> + + <!-- Default Search Engine --> + <groupbox id="defaultEngineGroup" align="start" data-category="paneSearch"> + <caption label="&defaultSearchEngine.label;"/> + <label>&chooseYourDefaultSearchEngine.label;</label> + <menulist id="defaultEngine"> + <menupopup/> + </menulist> + <checkbox id="suggestionsInSearchFieldsCheckbox" + label="&provideSearchSuggestions.label;" + accesskey="&provideSearchSuggestions.accesskey;" + preference="browser.search.suggest.enabled"/> + <vbox class="indent"> + <checkbox id="urlBarSuggestion" label="&showURLBarSuggestions.label;" + accesskey="&showURLBarSuggestions.accesskey;" + preference="browser.urlbar.suggest.searches"/> + <hbox id="urlBarSuggestionPermanentPBLabel" + align="center" class="indent"> + <label flex="1">&urlBarSuggestionsPermanentPB.label;</label> + </hbox> + </vbox> + </groupbox> + + <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch"> + <caption label="&oneClickSearchEngines.label;"/> + <label>&chooseWhichOneToDisplay.label;</label> + + <tree id="engineList" flex="1" rows="8" hidecolumnpicker="true" editable="true" + seltype="single"> + <treechildren id="engineChildren" flex="1"/> + <treecols> + <treecol id="engineShown" type="checkbox" editable="true" sortable="false"/> + <treecol id="engineName" flex="4" label="&engineNameColumn.label;" sortable="false"/> + <treecol id="engineKeyword" flex="1" label="&engineKeywordColumn.label;" editable="true" + sortable="false"/> + </treecols> + </tree> + + <hbox> + <button id="restoreDefaultSearchEngines" + label="&restoreDefaultSearchEngines.label;" + accesskey="&restoreDefaultSearchEngines.accesskey;" + /> + <spacer flex="1"/> + <button id="removeEngineButton" + class="searchEngineAction" + label="&removeEngine.label;" + accesskey="&removeEngine.accesskey;" + disabled="true" + /> + </hbox> + + <separator class="thin"/> + + <hbox id="addEnginesBox" pack="start"> + <label id="addEngines" class="text-link" value="&addMoreSearchEngines.label;"/> + </hbox> + </groupbox> diff --git a/browser/components/preferences/in-content/security.js b/browser/components/preferences/in-content/security.js new file mode 100644 index 000000000..a8ad28c7e --- /dev/null +++ b/browser/components/preferences/in-content/security.js @@ -0,0 +1,302 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); + +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var gSecurityPane = { + _pane: null, + + /** + * Initializes master password UI. + */ + init: function () + { + function setEventListener(aId, aEventType, aCallback) + { + document.getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gSecurityPane)); + } + + this._pane = document.getElementById("paneSecurity"); + this._initMasterPasswordUI(); + this._initSafeBrowsing(); + + setEventListener("addonExceptions", "command", + gSecurityPane.showAddonExceptions); + setEventListener("passwordExceptions", "command", + gSecurityPane.showPasswordExceptions); + setEventListener("useMasterPassword", "command", + gSecurityPane.updateMasterPasswordButton); + setEventListener("changeMasterPassword", "command", + gSecurityPane.changeMasterPassword); + setEventListener("showPasswords", "command", + gSecurityPane.showPasswords); + }, + + // ADD-ONS + + /* + * Preferences: + * + * xpinstall.whitelist.required + * - true if a site must be added to a site whitelist before extensions + * provided by the site may be installed from it, false if the extension + * may be directly installed after a confirmation dialog + */ + + /** + * Enables/disables the add-ons Exceptions button depending on whether + * or not add-on installation warnings are displayed. + */ + readWarnAddonInstall: function () + { + var warn = document.getElementById("xpinstall.whitelist.required"); + var exceptions = document.getElementById("addonExceptions"); + + exceptions.disabled = !warn.value; + + // don't override the preference value + return undefined; + }, + + /** + * Displays the exceptions lists for add-on installation warnings. + */ + showAddonExceptions: function () + { + var bundlePrefs = document.getElementById("bundlePreferences"); + + var params = this._addonParams; + if (!params.windowTitle || !params.introText) { + params.windowTitle = bundlePrefs.getString("addons_permissions_title"); + params.introText = bundlePrefs.getString("addonspermissionstext"); + } + + gSubDialog.open("chrome://browser/content/preferences/permissions.xul", + null, params); + }, + + /** + * Parameters for the add-on install permissions dialog. + */ + _addonParams: + { + blockVisible: false, + sessionVisible: false, + allowVisible: true, + prefilledHost: "", + permissionType: "install" + }, + + // PASSWORDS + + /* + * Preferences: + * + * signon.rememberSignons + * - true if passwords are remembered, false otherwise + */ + + /** + * Enables/disables the Exceptions button used to configure sites where + * passwords are never saved. When browser is set to start in Private + * Browsing mode, the "Remember passwords" UI is useless, so we disable it. + */ + readSavePasswords: function () + { + var pref = document.getElementById("signon.rememberSignons"); + var excepts = document.getElementById("passwordExceptions"); + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + document.getElementById("savePasswords").disabled = true; + excepts.disabled = true; + return false; + } + excepts.disabled = !pref.value; + // don't override pref value in UI + return undefined; + }, + + /** + * Displays a dialog in which the user can view and modify the list of sites + * where passwords are never saved. + */ + showPasswordExceptions: function () + { + var bundlePrefs = document.getElementById("bundlePreferences"); + var params = { + blockVisible: true, + sessionVisible: false, + allowVisible: false, + hideStatusColumn: true, + prefilledHost: "", + permissionType: "login-saving", + windowTitle: bundlePrefs.getString("savedLoginsExceptions_title"), + introText: bundlePrefs.getString("savedLoginsExceptions_desc") + }; + + gSubDialog.open("chrome://browser/content/preferences/permissions.xul", + null, 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: function () + { + var noMP = !LoginHelper.isMasterPasswordSet(); + + var button = document.getElementById("changeMasterPassword"); + button.disabled = noMP; + + var checkbox = document.getElementById("useMasterPassword"); + checkbox.checked = !noMP; + }, + + _initSafeBrowsing() { + let enableSafeBrowsing = document.getElementById("enableSafeBrowsing"); + let blockDownloads = document.getElementById("blockDownloads"); + let blockUncommonUnwanted = document.getElementById("blockUncommonUnwanted"); + + let safeBrowsingPhishingPref = document.getElementById("browser.safebrowsing.phishing.enabled"); + let safeBrowsingMalwarePref = document.getElementById("browser.safebrowsing.malware.enabled"); + + let blockDownloadsPref = document.getElementById("browser.safebrowsing.downloads.enabled"); + let malwareTable = document.getElementById("urlclassifier.malwareTable"); + + let blockUnwantedPref = document.getElementById("browser.safebrowsing.downloads.remote.block_potentially_unwanted"); + let blockUncommonPref = document.getElementById("browser.safebrowsing.downloads.remote.block_uncommon"); + + enableSafeBrowsing.addEventListener("command", function() { + safeBrowsingPhishingPref.value = enableSafeBrowsing.checked; + safeBrowsingMalwarePref.value = enableSafeBrowsing.checked; + + if (enableSafeBrowsing.checked) { + blockDownloads.removeAttribute("disabled"); + if (blockDownloads.checked) { + blockUncommonUnwanted.removeAttribute("disabled"); + } + } else { + blockDownloads.setAttribute("disabled", "true"); + blockUncommonUnwanted.setAttribute("disabled", "true"); + } + }); + + blockDownloads.addEventListener("command", function() { + blockDownloadsPref.value = blockDownloads.checked; + if (blockDownloads.checked) { + blockUncommonUnwanted.removeAttribute("disabled"); + } else { + blockUncommonUnwanted.setAttribute("disabled", "true"); + } + }); + + blockUncommonUnwanted.addEventListener("command", function() { + blockUnwantedPref.value = blockUncommonUnwanted.checked; + blockUncommonPref.value = blockUncommonUnwanted.checked; + + let malware = malwareTable.value + .split(",") + .filter(x => x !== "goog-unwanted-shavar" && x !== "test-unwanted-simple"); + + if (blockUncommonUnwanted.checked) { + malware.push("goog-unwanted-shavar"); + malware.push("test-unwanted-simple"); + } + + // sort alphabetically to keep the pref consistent + malware.sort(); + + malwareTable.value = malware.join(","); + }); + + // 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; + }, + + /** + * 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. + */ + updateMasterPasswordButton: function () + { + 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) + this._removeMasterPassword(); + else + 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. + */ + _removeMasterPassword: function () + { + var secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"]. + getService(Ci.nsIPKCS11ModuleDB); + if (secmodDB.isFIPSEnabled) { + var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + var bundle = document.getElementById("bundlePreferences"); + promptService.alert(window, + bundle.getString("pw_change_failed_title"), + bundle.getString("pw_change2empty_in_fips_mode")); + this._initMasterPasswordUI(); + } + else { + gSubDialog.open("chrome://mozapps/content/preferences/removemp.xul", + null, null, this._initMasterPasswordUI.bind(this)); + } + }, + + /** + * Displays a dialog in which the master password may be changed. + */ + changeMasterPassword: function () + { + gSubDialog.open("chrome://mozapps/content/preferences/changemp.xul", + "resizable=no", null, this._initMasterPasswordUI.bind(this)); + }, + + /** + * Shows the sites where the user has saved passwords and the associated login + * information. + */ + showPasswords: function () + { + gSubDialog.open("chrome://passwordmgr/content/passwordManager.xul"); + } + +}; diff --git a/browser/components/preferences/in-content/security.xul b/browser/components/preferences/in-content/security.xul new file mode 100644 index 000000000..a10576c25 --- /dev/null +++ b/browser/components/preferences/in-content/security.xul @@ -0,0 +1,131 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<!-- Security panel --> + +<script type="application/javascript" + src="chrome://browser/content/preferences/in-content/security.js"/> + +<preferences id="securityPreferences" hidden="true" data-category="paneSecurity"> + <!-- XXX buttons --> + <preference id="pref.privacy.disable_button.view_passwords" + name="pref.privacy.disable_button.view_passwords" + type="bool"/> + <preference id="pref.privacy.disable_button.view_passwords_exceptions" + name="pref.privacy.disable_button.view_passwords_exceptions" + type="bool"/> + + <!-- Add-ons, malware, phishing --> + <preference id="xpinstall.whitelist.required" + name="xpinstall.whitelist.required" + type="bool"/> + + <preference id="browser.safebrowsing.malware.enabled" + name="browser.safebrowsing.malware.enabled" + type="bool"/> + <preference id="browser.safebrowsing.phishing.enabled" + name="browser.safebrowsing.phishing.enabled" + type="bool"/> + + <preference id="browser.safebrowsing.downloads.enabled" + name="browser.safebrowsing.downloads.enabled" + type="bool"/> + + <preference id="urlclassifier.malwareTable" + name="urlclassifier.malwareTable" + type="string"/> + + <preference id="browser.safebrowsing.downloads.remote.block_potentially_unwanted" + name="browser.safebrowsing.downloads.remote.block_potentially_unwanted" + type="bool"/> + <preference id="browser.safebrowsing.downloads.remote.block_uncommon" + name="browser.safebrowsing.downloads.remote.block_uncommon" + type="bool"/> + + <!-- Passwords --> + <preference id="signon.rememberSignons" name="signon.rememberSignons" type="bool"/> + +</preferences> + +<hbox id="header-security" + class="header" + hidden="true" + data-category="paneSecurity"> + <label class="header-name" flex="1">&paneSecurity.title;</label> + <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a> +</hbox> + +<!-- addons, forgery (phishing) UI --> +<groupbox id="addonsPhishingGroup" data-category="paneSecurity" hidden="true"> + <caption><label>&general.label;</label></caption> + + <hbox id="addonInstallBox"> + <checkbox id="warnAddonInstall" + label="&warnAddonInstall.label;" + accesskey="&warnAddonInstall.accesskey;" + preference="xpinstall.whitelist.required" + onsyncfrompreference="return gSecurityPane.readWarnAddonInstall();"/> + <spacer flex="1"/> + <button id="addonExceptions" + label="&addonExceptions.label;" + accesskey="&addonExceptions.accesskey;"/> + </hbox> + + <separator class="thin"/> + <vbox align="start"> + <checkbox id="enableSafeBrowsing" + label="&enableSafeBrowsing.label;" + accesskey="&enableSafeBrowsing.accesskey;" /> + <vbox class="indent"> + <checkbox id="blockDownloads" + label="&blockDownloads.label;" + accesskey="&blockDownloads.accesskey;" /> + <checkbox id="blockUncommonUnwanted" + label="&blockUncommonUnwanted.label;" + accesskey="&blockUncommonUnwanted.accesskey;" /> + </vbox> + </vbox> +</groupbox> + +<!-- Passwords --> +<groupbox id="passwordsGroup" orient="vertical" data-category="paneSecurity" hidden="true"> + <caption><label>&logins.label;</label></caption> + + <hbox id="savePasswordsBox"> + <checkbox id="savePasswords" + label="&rememberLogins.label;" accesskey="&rememberLogins.accesskey;" + preference="signon.rememberSignons" + onsyncfrompreference="return gSecurityPane.readSavePasswords();"/> + <spacer flex="1"/> + <button id="passwordExceptions" + label="&passwordExceptions.label;" + accesskey="&passwordExceptions.accesskey;" + preference="pref.privacy.disable_button.view_passwords_exceptions"/> + </hbox> + <grid id="passwordGrid"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows id="passwordRows"> + <row id="masterPasswordRow"> + <hbox id="masterPasswordBox"> + <checkbox id="useMasterPassword" + label="&useMasterPassword.label;" + accesskey="&useMasterPassword.accesskey;"/> + <spacer flex="1"/> + </hbox> + <button id="changeMasterPassword" + label="&changeMasterPassword.label;" + accesskey="&changeMasterPassword.accesskey;"/> + </row> + <row id="showPasswordRow"> + <hbox id="showPasswordsBox"/> + <button id="showPasswords" + label="&savedLogins.label;" accesskey="&savedLogins.accesskey;" + preference="pref.privacy.disable_button.view_passwords"/> + </row> + </rows> + </grid> +</groupbox> diff --git a/browser/components/preferences/in-content/subdialogs.js b/browser/components/preferences/in-content/subdialogs.js new file mode 100644 index 000000000..bb8d0048f --- /dev/null +++ b/browser/components/preferences/in-content/subdialogs.js @@ -0,0 +1,434 @@ +/* - This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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 gSubDialog = { + _closingCallback: null, + _closingEvent: null, + _isClosing: false, + _frame: null, + _overlay: null, + _box: null, + _injectedStyleSheets: [ + "chrome://browser/skin/preferences/preferences.css", + "chrome://global/skin/in-content/common.css", + "chrome://browser/skin/preferences/in-content/preferences.css", + "chrome://browser/skin/preferences/in-content/dialog.css", + ], + _resizeObserver: null, + + init: function() { + this._frame = document.getElementById("dialogFrame"); + this._overlay = document.getElementById("dialogOverlay"); + this._box = document.getElementById("dialogBox"); + this._closeButton = document.getElementById("dialogClose"); + }, + + updateTitle: function(aEvent) { + if (aEvent.target != gSubDialog._frame.contentDocument) + return; + document.getElementById("dialogTitle").textContent = gSubDialog._frame.contentDocument.title; + }, + + injectXMLStylesheet: function(aStylesheetURL) { + let contentStylesheet = this._frame.contentDocument.createProcessingInstruction( + 'xml-stylesheet', + 'href="' + aStylesheetURL + '" type="text/css"' + ); + this._frame.contentDocument.insertBefore(contentStylesheet, + this._frame.contentDocument.documentElement); + }, + + open: function(aURL, aFeatures = null, aParams = null, aClosingCallback = null) { + // If we're already open/opening on this URL, do nothing. + if (this._openedURL == aURL && !this._isClosing) { + return; + } + // If we're open on some (other) URL or we're closing, open when closing has finished. + if (this._openedURL || this._isClosing) { + if (!this._isClosing) { + this.close(); + } + let args = Array.from(arguments); + this._closingPromise.then(() => { + this.open.apply(this, args); + }); + return; + } + this._addDialogEventListeners(); + + let features = (aFeatures ? aFeatures + "," : "") + "resizable,dialog=no,centerscreen"; + let dialog = window.openDialog(aURL, "dialogFrame", features, aParams); + if (aClosingCallback) { + this._closingCallback = aClosingCallback.bind(dialog); + } + + this._closingEvent = null; + this._isClosing = false; + this._openedURL = aURL; + + features = features.replace(/,/g, "&"); + let featureParams = new URLSearchParams(features.toLowerCase()); + this._box.setAttribute("resizable", featureParams.has("resizable") && + featureParams.get("resizable") != "no" && + featureParams.get("resizable") != "0"); + }, + + close: function(aEvent = null) { + if (this._isClosing) { + return; + } + this._isClosing = true; + this._closingPromise = new Promise(resolve => { + this._resolveClosePromise = resolve; + }); + + if (this._closingCallback) { + try { + this._closingCallback.call(null, aEvent); + } catch (ex) { + Cu.reportError(ex); + } + this._closingCallback = null; + } + + this._removeDialogEventListeners(); + + this._overlay.style.visibility = ""; + // Clear the sizing inline styles. + this._frame.removeAttribute("style"); + // Clear the sizing attributes + this._box.removeAttribute("width"); + this._box.removeAttribute("height"); + this._box.style.removeProperty("min-height"); + this._box.style.removeProperty("min-width"); + + setTimeout(() => { + // Unload the dialog after the event listeners run so that the load of about:blank isn't + // cancelled by the ESC <key>. + let onBlankLoad = e => { + if (this._frame.contentWindow.location.href == "about:blank") { + this._frame.removeEventListener("load", onBlankLoad); + // We're now officially done closing, so update the state to reflect that. + delete this._openedURL; + this._isClosing = false; + this._resolveClosePromise(); + } + }; + this._frame.addEventListener("load", onBlankLoad); + this._frame.loadURI("about:blank"); + }, 0); + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "command": + this._frame.contentWindow.close(); + break; + case "dialogclosing": + this._onDialogClosing(aEvent); + break; + case "DOMTitleChanged": + this.updateTitle(aEvent); + break; + case "DOMFrameContentLoaded": + this._onContentLoaded(aEvent); + break; + case "load": + this._onLoad(aEvent); + break; + case "unload": + this._onUnload(aEvent); + break; + case "keydown": + this._onKeyDown(aEvent); + break; + case "focus": + this._onParentWinFocus(aEvent); + break; + } + }, + + /* Private methods */ + + _onUnload: function(aEvent) { + if (aEvent.target.location.href == this._openedURL) { + this._frame.contentWindow.close(); + } + }, + + _onContentLoaded: function(aEvent) { + if (aEvent.target != this._frame || aEvent.target.contentWindow.location == "about:blank") { + return; + } + + for (let styleSheetURL of this._injectedStyleSheets) { + this.injectXMLStylesheet(styleSheetURL); + } + + // Provide the ability for the dialog to know that it is being loaded "in-content". + this._frame.contentDocument.documentElement.setAttribute("subdialog", "true"); + + this._frame.contentWindow.addEventListener("dialogclosing", this); + + let oldResizeBy = this._frame.contentWindow.resizeBy; + this._frame.contentWindow.resizeBy = function(resizeByWidth, resizeByHeight) { + // Only handle resizeByHeight currently. + let frameHeight = gSubDialog._frame.clientHeight; + let boxMinHeight = parseFloat(getComputedStyle(gSubDialog._box).minHeight, 10); + + gSubDialog._frame.style.height = (frameHeight + resizeByHeight) + "px"; + gSubDialog._box.style.minHeight = (boxMinHeight + resizeByHeight) + "px"; + + oldResizeBy.call(gSubDialog._frame.contentWindow, resizeByWidth, resizeByHeight); + }; + + // Make window.close calls work like dialog closing. + let oldClose = this._frame.contentWindow.close; + this._frame.contentWindow.close = function() { + var closingEvent = gSubDialog._closingEvent; + if (!closingEvent) { + closingEvent = new CustomEvent("dialogclosing", { + bubbles: true, + detail: { button: null }, + }); + + gSubDialog._frame.contentWindow.dispatchEvent(closingEvent); + } + + gSubDialog.close(closingEvent); + oldClose.call(gSubDialog._frame.contentWindow); + }; + + // XXX: Hack to make focus during the dialog's load functions work. Make the element visible + // sooner in DOMContentLoaded but mostly invisible instead of changing visibility just before + // the dialog's load event. + this._overlay.style.visibility = "visible"; + this._overlay.style.opacity = "0.01"; + }, + + _onLoad: function(aEvent) { + if (aEvent.target.contentWindow.location == "about:blank") { + return; + } + + // Do this on load to wait for the CSS to load and apply before calculating the size. + let docEl = this._frame.contentDocument.documentElement; + + let groupBoxTitle = document.getAnonymousElementByAttribute(this._box, "class", "groupbox-title"); + let groupBoxTitleHeight = groupBoxTitle.clientHeight + + parseFloat(getComputedStyle(groupBoxTitle).borderBottomWidth); + + let groupBoxBody = document.getAnonymousElementByAttribute(this._box, "class", "groupbox-body"); + // These are deduced from styles which we don't change, so it's safe to get them now: + let boxVerticalPadding = 2 * parseFloat(getComputedStyle(groupBoxBody).paddingTop); + let boxHorizontalPadding = 2 * parseFloat(getComputedStyle(groupBoxBody).paddingLeft); + let boxHorizontalBorder = 2 * parseFloat(getComputedStyle(this._box).borderLeftWidth); + let boxVerticalBorder = 2 * parseFloat(getComputedStyle(this._box).borderTopWidth); + + // The difference between the frame and box shouldn't change, either: + let boxRect = this._box.getBoundingClientRect(); + let frameRect = this._frame.getBoundingClientRect(); + let frameSizeDifference = (frameRect.top - boxRect.top) + (boxRect.bottom - frameRect.bottom); + + // Then determine and set a bunch of width stuff: + let frameMinWidth = docEl.style.width || docEl.scrollWidth + "px"; + let frameWidth = docEl.getAttribute("width") ? docEl.getAttribute("width") + "px" : + frameMinWidth; + this._frame.style.width = frameWidth; + this._box.style.minWidth = "calc(" + + (boxHorizontalBorder + boxHorizontalPadding) + + "px + " + frameMinWidth + ")"; + + // Now do the same but for the height. We need to do this afterwards because otherwise + // XUL assumes we'll optimize for height and gives us "wrong" values which then are no + // longer correct after we set the width: + let frameMinHeight = docEl.style.height || docEl.scrollHeight + "px"; + let frameHeight = docEl.getAttribute("height") ? docEl.getAttribute("height") + "px" : + frameMinHeight; + + // Now check if the frame height we calculated is possible at this window size, + // accounting for titlebar, padding/border and some spacing. + let maxHeight = window.innerHeight - frameSizeDifference - 30; + // Do this with a frame height in pixels... + let comparisonFrameHeight; + if (frameHeight.endsWith("em")) { + let fontSize = parseFloat(getComputedStyle(this._frame).fontSize); + comparisonFrameHeight = parseFloat(frameHeight, 10) * fontSize; + } else if (frameHeight.endsWith("px")) { + comparisonFrameHeight = parseFloat(frameHeight, 10); + } else { + Cu.reportError("This dialog (" + this._frame.contentWindow.location.href + ") " + + "set a height in non-px-non-em units ('" + frameHeight + "'), " + + "which is likely to lead to bad sizing in in-content preferences. " + + "Please consider changing this."); + comparisonFrameHeight = parseFloat(frameHeight); + } + + if (comparisonFrameHeight > maxHeight) { + // If the height is bigger than that of the window, we should let the contents scroll: + frameHeight = maxHeight + "px"; + frameMinHeight = maxHeight + "px"; + let containers = this._frame.contentDocument.querySelectorAll('.largeDialogContainer'); + for (let container of containers) { + container.classList.add("doScroll"); + } + } + + this._frame.style.height = frameHeight; + this._box.style.minHeight = "calc(" + + (boxVerticalBorder + groupBoxTitleHeight + boxVerticalPadding) + + "px + " + frameMinHeight + ")"; + + this._overlay.style.visibility = "visible"; + this._overlay.style.opacity = ""; // XXX: focus hack continued from _onContentLoaded + + if (this._box.getAttribute("resizable") == "true") { + this._resizeObserver = new MutationObserver(this._onResize); + this._resizeObserver.observe(this._box, {attributes: true}); + } + + this._trapFocus(); + }, + + _onResize: function(mutations) { + let frame = gSubDialog._frame; + // The width and height styles are needed for the initial + // layout of the frame, but afterward they need to be removed + // or their presence will restrict the contents of the <browser> + // from resizing to a smaller size. + frame.style.removeProperty("width"); + frame.style.removeProperty("height"); + + let docEl = frame.contentDocument.documentElement; + let persistedAttributes = docEl.getAttribute("persist"); + if (!persistedAttributes || + (!persistedAttributes.includes("width") && + !persistedAttributes.includes("height"))) { + return; + } + + for (let mutation of mutations) { + if (mutation.attributeName == "width") { + docEl.setAttribute("width", docEl.scrollWidth); + } else if (mutation.attributeName == "height") { + docEl.setAttribute("height", docEl.scrollHeight); + } + } + }, + + _onDialogClosing: function(aEvent) { + this._frame.contentWindow.removeEventListener("dialogclosing", this); + this._closingEvent = aEvent; + }, + + _onKeyDown: function(aEvent) { + if (aEvent.currentTarget == window && aEvent.keyCode == aEvent.DOM_VK_ESCAPE && + !aEvent.defaultPrevented) { + this.close(aEvent); + return; + } + if (aEvent.keyCode != aEvent.DOM_VK_TAB || + aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) { + return; + } + + let fm = Services.focus; + + function isLastFocusableElement(el) { + // XXXgijs unfortunately there is no way to get the last focusable element without asking + // the focus manager to move focus to it. + let rv = el == fm.moveFocus(gSubDialog._frame.contentWindow, null, fm.MOVEFOCUS_LAST, 0); + fm.setFocus(el, 0); + return rv; + } + + let forward = !aEvent.shiftKey; + // check if focus is leaving the frame (incl. the close button): + if ((aEvent.target == this._closeButton && !forward) || + (isLastFocusableElement(aEvent.originalTarget) && forward)) { + aEvent.preventDefault(); + aEvent.stopImmediatePropagation(); + let parentWin = this._getBrowser().ownerGlobal; + if (forward) { + fm.moveFocus(parentWin, null, fm.MOVEFOCUS_FIRST, fm.FLAG_BYKEY); + } else { + // Somehow, moving back 'past' the opening doc is not trivial. Cheat by doing it in 2 steps: + fm.moveFocus(window, null, fm.MOVEFOCUS_ROOT, fm.FLAG_BYKEY); + fm.moveFocus(parentWin, null, fm.MOVEFOCUS_BACKWARD, fm.FLAG_BYKEY); + } + } + }, + + _onParentWinFocus: function(aEvent) { + // Explicitly check for the focus target of |window| to avoid triggering this when the window + // is refocused + if (aEvent.target != this._closeButton && aEvent.target != window) { + this._closeButton.focus(); + } + }, + + _addDialogEventListeners: function() { + // Make the close button work. + this._closeButton.addEventListener("command", this); + + // DOMTitleChanged isn't fired on the frame, only on the chromeEventHandler + let chromeBrowser = this._getBrowser(); + chromeBrowser.addEventListener("DOMTitleChanged", this, true); + + // Similarly DOMFrameContentLoaded only fires on the top window + window.addEventListener("DOMFrameContentLoaded", this, true); + + // Wait for the stylesheets injected during DOMContentLoaded to load before showing the dialog + // otherwise there is a flicker of the stylesheet applying. + this._frame.addEventListener("load", this); + + chromeBrowser.addEventListener("unload", this, true); + // Ensure we get <esc> keypresses even if nothing in the subdialog is focusable + // (happens on OS X when only text inputs and lists are focusable, and + // the subdialog only has checkboxes/radiobuttons/buttons) + window.addEventListener("keydown", this, true); + }, + + _removeDialogEventListeners: function() { + let chromeBrowser = this._getBrowser(); + chromeBrowser.removeEventListener("DOMTitleChanged", this, true); + chromeBrowser.removeEventListener("unload", this, true); + + this._closeButton.removeEventListener("command", this); + + window.removeEventListener("DOMFrameContentLoaded", this, true); + this._frame.removeEventListener("load", this); + this._frame.contentWindow.removeEventListener("dialogclosing", this); + window.removeEventListener("keydown", this, true); + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + this._resizeObserver = null; + } + this._untrapFocus(); + }, + + _trapFocus: function() { + let fm = Services.focus; + fm.moveFocus(this._frame.contentWindow, null, fm.MOVEFOCUS_FIRST, 0); + this._frame.contentDocument.addEventListener("keydown", this, true); + this._closeButton.addEventListener("keydown", this); + + window.addEventListener("focus", this, true); + }, + + _untrapFocus: function() { + this._frame.contentDocument.removeEventListener("keydown", this, true); + this._closeButton.removeEventListener("keydown", this); + window.removeEventListener("focus", this); + }, + + _getBrowser: function() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + }, +}; diff --git a/browser/components/preferences/in-content/sync.js b/browser/components/preferences/in-content/sync.js new file mode 100644 index 000000000..27f7cd48c --- /dev/null +++ b/browser/components/preferences/in-content/sync.js @@ -0,0 +1,673 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://services-sync/main.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () { + return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {}); +}); + +XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", + "resource://gre/modules/FxAccounts.jsm"); + +const PAGE_NO_ACCOUNT = 0; +const PAGE_HAS_ACCOUNT = 1; +const PAGE_NEEDS_UPDATE = 2; +const FXA_PAGE_LOGGED_OUT = 3; +const FXA_PAGE_LOGGED_IN = 4; + +// 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; + +var gSyncPane = { + prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs", + "engine.tabs", "engine.history"], + + get page() { + return document.getElementById("weavePrefsDeck").selectedIndex; + }, + + set page(val) { + document.getElementById("weavePrefsDeck").selectedIndex = val; + }, + + get _usingCustomServer() { + return Weave.Svc.Prefs.isSet("serverURL"); + }, + + needsUpdate: function () { + this.page = PAGE_NEEDS_UPDATE; + let label = document.getElementById("loginError"); + label.textContent = Weave.Utils.getErrorString(Weave.Status.login); + label.className = "error"; + }, + + init: function () { + this._setupEventListeners(); + + // If the Service hasn't finished initializing, wait for it. + let xps = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + + if (xps.ready) { + this._init(); + return; + } + + // it may take some time before we can determine what provider to use + // and the state of that provider, so show the "please wait" page. + this._showLoadPage(xps); + + let onUnload = function () { + window.removeEventListener("unload", onUnload, false); + try { + Services.obs.removeObserver(onReady, "weave:service:ready"); + } catch (e) {} + }; + + let onReady = function () { + Services.obs.removeObserver(onReady, "weave:service:ready"); + window.removeEventListener("unload", onUnload, false); + this._init(); + }.bind(this); + + Services.obs.addObserver(onReady, "weave:service:ready", false); + window.addEventListener("unload", onUnload, false); + + xps.ensureLoaded(); + }, + + _showLoadPage: function (xps) { + let username; + try { + username = Services.prefs.getCharPref("services.sync.username"); + } catch (e) {} + if (!username) { + this.page = FXA_PAGE_LOGGED_OUT; + } else if (xps.fxAccountsEnabled) { + // Use cached values while we wait for the up-to-date values + let cachedComputerName; + try { + cachedComputerName = Services.prefs.getCharPref("services.sync.client.name"); + } + catch (e) { + cachedComputerName = ""; + } + document.getElementById("fxaEmailAddress1").textContent = username; + this._populateComputerName(cachedComputerName); + this.page = FXA_PAGE_LOGGED_IN; + } else { // Old Sync + this.page = PAGE_HAS_ACCOUNT; + } + }, + + _init: function () { + let topics = ["weave:service:login:error", + "weave:service:login:finish", + "weave:service:start-over:finish", + "weave:service:setup-complete", + "weave:service:logout:finish", + FxAccountsCommon.ONVERIFIED_NOTIFICATION, + FxAccountsCommon.ONLOGIN_NOTIFICATION, + FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, + ]; + // Add the observers now and remove them on unload + // XXXzpao This should use Services.obs.* but Weave's Obs does nice handling + // of `this`. Fix in a followup. (bug 583347) + topics.forEach(function (topic) { + Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this); + }, this); + + window.addEventListener("unload", function() { + topics.forEach(function (topic) { + Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this); + }, gSyncPane); + }, false); + + XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => { + return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties"); + }); + + XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => { + return Services.strings.createBundle("chrome://browser/locale/accounts.properties"); + }); + + let url = Services.prefs.getCharPref("identity.mobilepromo.android") + "sync-preferences"; + document.getElementById("fxaMobilePromo-android").setAttribute("href", url); + document.getElementById("fxaMobilePromo-android-hasFxaAccount").setAttribute("href", url); + url = Services.prefs.getCharPref("identity.mobilepromo.ios") + "sync-preferences"; + document.getElementById("fxaMobilePromo-ios").setAttribute("href", url); + document.getElementById("fxaMobilePromo-ios-hasFxaAccount").setAttribute("href", url); + + document.getElementById("tosPP-small-ToS").setAttribute("href", gSyncUtils.tosURL); + document.getElementById("tosPP-normal-ToS").setAttribute("href", gSyncUtils.tosURL); + document.getElementById("tosPP-small-PP").setAttribute("href", gSyncUtils.privacyPolicyURL); + document.getElementById("tosPP-normal-PP").setAttribute("href", gSyncUtils.privacyPolicyURL); + + fxAccounts.promiseAccountsManageURI(this._getEntryPoint()).then(url => { + document.getElementById("verifiedManage").setAttribute("href", url); + }); + + this.updateWeavePrefs(); + + this._initProfileImageUI(); + }, + + _toggleComputerNameControls: function(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: function() { + let textbox = document.getElementById("fxaSyncComputerName"); + let valLength = textbox.value.length; + textbox.focus(); + textbox.setSelectionRange(valLength, valLength); + }, + + _blurComputerNameTextbox: function() { + document.getElementById("fxaSyncComputerName").blur(); + }, + + _focusAfterComputerNameTextbox: function() { + // 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: function(save) { + if (save) { + let textbox = document.getElementById("fxaSyncComputerName"); + Weave.Service.clientsEngine.localName = textbox.value; + } + this._populateComputerName(Weave.Service.clientsEngine.localName); + }, + + _setupEventListeners: function() { + function setEventListener(aId, aEventType, aCallback) + { + document.getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gSyncPane)); + } + + setEventListener("noAccountSetup", "click", function (aEvent) { + aEvent.stopPropagation(); + gSyncPane.openSetup(null); + }); + setEventListener("noAccountPair", "click", function (aEvent) { + aEvent.stopPropagation(); + gSyncPane.openSetup('pair'); + }); + setEventListener("syncChangePassword", "command", + () => gSyncUtils.changePassword()); + setEventListener("syncResetPassphrase", "command", + () => gSyncUtils.resetPassphrase()); + setEventListener("syncReset", "command", gSyncPane.resetSync); + setEventListener("syncAddDeviceLabel", "click", function () { + gSyncPane.openAddDevice(); + return false; + }); + setEventListener("syncEnginesList", "select", function () { + if (this.selectedCount) + this.clearSelection(); + }); + setEventListener("syncComputerName", "change", function (e) { + gSyncUtils.changeName(e.target); + }); + 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("unlinkDevice", "click", function () { + gSyncPane.startOver(true); + return false; + }); + setEventListener("loginErrorUpdatePass", "click", function () { + gSyncPane.updatePass(); + return false; + }); + setEventListener("loginErrorResetPass", "click", function () { + gSyncPane.resetPass(); + return false; + }); + setEventListener("loginErrorStartOver", "click", function () { + gSyncPane.startOver(true); + return false; + }); + setEventListener("noFxaSignUp", "command", function () { + gSyncPane.signUp(); + return false; + }); + 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(); + } + }); + }, + + _initProfileImageUI: function () { + try { + if (Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled")) { + document.getElementById("fxaProfileImage").hidden = false; + } + } catch (e) { } + }, + + updateWeavePrefs: function () { + let service = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + // service.fxAccountsEnabled is false iff sync is already configured for + // the legacy provider. + if (service.fxAccountsEnabled) { + let displayNameLabel = document.getElementById("fxaDisplayName"); + let fxaEmailAddress1Label = document.getElementById("fxaEmailAddress1"); + fxaEmailAddress1Label.hidden = false; + displayNameLabel.hidden = true; + + let profileInfoEnabled; + try { + profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled"); + } catch (ex) {} + + // determine the fxa status... + this._showLoadPage(service); + + fxAccounts.getSignedInUser().then(data => { + if (!data) { + this.page = FXA_PAGE_LOGGED_OUT; + return false; + } + 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; + // Not Verfied implies login error state, so check that first. + if (!data.verified) { + fxaLoginStatus.selectedIndex = FXA_LOGIN_UNVERIFIED; + syncReady = false; + // So we think we are logged in, so login problems are next. + // (Although if the Sync identity manager is still initializing, we + // ignore login errors and assume all will eventually be good.) + // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in". + // All other login failures are assumed to be transient and should go + // away by themselves, so aren't reflected here. + } else if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { + fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED; + syncReady = false; + // Else we must be golden (or in an error state we expect to magically + // resolve itself) + } else { + fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED; + syncReady = true; + } + fxaEmailAddress1Label.textContent = data.email; + document.getElementById("fxaEmailAddress2").textContent = data.email; + document.getElementById("fxaEmailAddress3").textContent = data.email; + this._populateComputerName(Weave.Service.clientsEngine.localName); + let engines = document.getElementById("fxaSyncEngines") + for (let checkbox of engines.querySelectorAll("checkbox")) { + checkbox.disabled = !syncReady; + } + document.getElementById("fxaChangeDeviceName").disabled = !syncReady; + + // Clear the profile image (if any) of the previously logged in account. + document.getElementById("fxaProfileImage").style.removeProperty("list-style-image"); + + // If the account is verified the next promise in the chain will + // fetch profile data. + return data.verified; + }).then(isVerified => { + if (isVerified) { + return fxAccounts.getSignedInUserProfile(); + } + return null; + }).then(data => { + let fxaLoginStatus = document.getElementById("fxaLoginStatus"); + if (data && profileInfoEnabled) { + if (data.displayName) { + fxaLoginStatus.setAttribute("hasName", true); + displayNameLabel.hidden = false; + displayNameLabel.textContent = data.displayName; + } else { + fxaLoginStatus.removeAttribute("hasName"); + } + if (data.avatar) { + let bgImage = "url(\"" + data.avatar + "\")"; + let profileImageElement = document.getElementById("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 = data.avatar; + } + } else { + fxaLoginStatus.removeAttribute("hasName"); + } + }, err => { + FxAccountsCommon.log.error(err); + }).catch(err => { + // If we get here something's really busted + Cu.reportError(String(err)); + }); + + // If fxAccountEnabled is false and we are in a "not configured" state, + // then fxAccounts is probably fully disabled rather than just unconfigured, + // so handle this case. This block can be removed once we remove support + // for fxAccounts being disabled. + } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED || + Weave.Svc.Prefs.get("firstSync", "") == "notReady") { + this.page = PAGE_NO_ACCOUNT; + // else: sync was previously configured for the legacy provider, so we + // make the "old" panels available. + } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE || + Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { + this.needsUpdate(); + } else { + this.page = PAGE_HAS_ACCOUNT; + document.getElementById("accountName").textContent = Weave.Service.identity.account; + document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName; + document.getElementById("tosPP-normal").hidden = this._usingCustomServer; + } + }, + + startOver: function (showDialog) { + if (showDialog) { + let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + + Services.prompt.BUTTON_POS_1_DEFAULT; + let buttonChoice = + Services.prompt.confirmEx(window, + this._stringBundle.GetStringFromName("syncUnlink.title"), + this._stringBundle.GetStringFromName("syncUnlink.label"), + flags, + this._stringBundle.GetStringFromName("syncUnlinkConfirm.label"), + null, null, null, {}); + + // If the user selects cancel, just bail + if (buttonChoice == 1) + return; + } + + Weave.Service.startOver(); + this.updateWeavePrefs(); + }, + + updatePass: function () { + if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) + gSyncUtils.changePassword(); + else + gSyncUtils.updatePassphrase(); + }, + + resetPass: function () { + if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) + gSyncUtils.resetPassword(); + else + gSyncUtils.resetPassphrase(); + }, + + _getEntryPoint: function () { + let params = new URLSearchParams(document.URL.split("#")[0].split("?")[1] || ""); + return params.get("entrypoint") || "preferences"; + }, + + _openAboutAccounts: function(action) { + let entryPoint = this._getEntryPoint(); + let params = new URLSearchParams(); + if (action) { + params.set("action", action); + } + params.set("entrypoint", entryPoint); + + this.replaceTabWithUrl("about:accounts?" + params); + }, + + /** + * Invoke the Sync setup wizard. + * + * @param wizardType + * Indicates type of wizard to launch: + * null -- regular set up wizard + * "pair" -- pair a device first + * "reset" -- reset sync + */ + openSetup: function (wizardType) { + let service = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + + if (service.fxAccountsEnabled) { + this._openAboutAccounts(); + } else { + let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); + if (win) + win.focus(); + else { + window.openDialog("chrome://browser/content/sync/setup.xul", + "weaveSetup", "centerscreen,chrome,resizable=no", + wizardType); + } + } + }, + + openContentInBrowser: function(url, options) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + if (!win) { + // no window to use, so use _openLink to create a new one. We don't + // always use that as it prefers to open a new window rather than use + // an existing one. + gSyncUtils._openLink(url); + 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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + // And tell it to load our URL. + browser.loadURI(url); + }, + + signUp: function() { + this._openAboutAccounts("signup"); + }, + + signIn: function() { + this._openAboutAccounts("signin"); + }, + + reSignIn: function() { + this._openAboutAccounts("reauth"); + }, + + + clickOrSpaceOrEnterPressed: function(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: function(event) { + if (this.clickOrSpaceOrEnterPressed(event)) { + fxAccounts.promiseAccountsChangeProfileURI(this._getEntryPoint(), "avatar") + .then(url => { + this.openContentInBrowser(url, { + replaceQueryString: true + }); + }); + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + }, + + openManageFirefoxAccount: function(event) { + if (this.clickOrSpaceOrEnterPressed(event)) { + this.manageFirefoxAccount(); + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + }, + + manageFirefoxAccount: function() { + fxAccounts.promiseAccountsManageURI(this._getEntryPoint()) + .then(url => { + this.openContentInBrowser(url, { + replaceQueryString: true + }); + }); + }, + + verifyFirefoxAccount: function() { + let showVerifyNotification = (data) => { + let isError = !data; + let maybeNot = isError ? "Not" : ""; + let sb = this._accountsStringBundle; + let title = sb.GetStringFromName("verification" + maybeNot + "SentTitle"); + let email = !isError && data ? data.email : ""; + let body = sb.formatStringFromName("verification" + maybeNot + "SentBody", [email], 1); + new Notification(title, { body }) + } + + let onError = () => { + showVerifyNotification(); + }; + + let onSuccess = data => { + if (data) { + showVerifyNotification(data); + } else { + onError(); + } + }; + + fxAccounts.resendVerificationEmail() + .then(fxAccounts.getSignedInUser, onError) + .then(onSuccess, onError); + }, + + openOldSyncSupportPage: function() { + let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync"; + this.openContentInBrowser(url); + }, + + unlinkFirefoxAccount: function(confirm) { + if (confirm) { + // We use a string bundle shared with aboutAccounts. + let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); + let disconnectLabel = sb.GetStringFromName("disconnect.label"); + let title = sb.GetStringFromName("disconnect.verify.title"); + let body = sb.GetStringFromName("disconnect.verify.bodyHeading") + + "\n\n" + + sb.GetStringFromName("disconnect.verify.bodyText"); + let ps = Services.prompt; + let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) + + (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) + + ps.BUTTON_POS_1_DEFAULT; + + let factory = Cc["@mozilla.org/prompter;1"] + .getService(Ci.nsIPromptFactory); + let prompt = factory.getPrompt(window, Ci.nsIPrompt); + let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2); + bag.setPropertyAsBool("allowTabModal", true); + + let pressed = prompt.confirmEx(title, body, buttonFlags, + disconnectLabel, null, null, null, {}); + + if (pressed != 0) { // 0 is the "continue" button + return; + } + } + fxAccounts.signOut().then(() => { + this.updateWeavePrefs(); + }); + }, + + openAddDevice: function () { + if (!Weave.Utils.ensureMPUnlocked()) + return; + + let win = Services.wm.getMostRecentWindow("Sync:AddDevice"); + if (win) + win.focus(); + else + window.openDialog("chrome://browser/content/sync/addDevice.xul", + "syncAddDevice", "centerscreen,chrome,resizable=no"); + }, + + resetSync: function () { + this.openSetup("reset"); + }, + + _populateComputerName(value) { + let textbox = document.getElementById("fxaSyncComputerName"); + if (!textbox.hasAttribute("placeholder")) { + textbox.setAttribute("placeholder", + Weave.Utils.getDefaultDeviceName()); + } + textbox.value = value; + }, +}; diff --git a/browser/components/preferences/in-content/sync.xul b/browser/components/preferences/in-content/sync.xul new file mode 100644 index 000000000..f1aebf2aa --- /dev/null +++ b/browser/components/preferences/in-content/sync.xul @@ -0,0 +1,359 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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 --> + +<preferences id="syncEnginePrefs" hidden="true" data-category="paneSync"> + <preference id="engine.addons" + name="services.sync.engine.addons" + type="bool"/> + <preference id="engine.bookmarks" + name="services.sync.engine.bookmarks" + type="bool"/> + <preference id="engine.history" + name="services.sync.engine.history" + type="bool"/> + <preference id="engine.tabs" + name="services.sync.engine.tabs" + type="bool"/> + <preference id="engine.prefs" + name="services.sync.engine.prefs" + type="bool"/> + <preference id="engine.passwords" + name="services.sync.engine.passwords" + type="bool"/> +</preferences> + +<script type="application/javascript" + src="chrome://browser/content/preferences/in-content/sync.js"/> +<script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + +<hbox id="header-sync" + class="header" + hidden="true" + data-category="paneSync"> + <label class="header-name" flex="1">&paneSync.title;</label> + <html:a class="help-button text-link" target="_blank" aria-label="&helpButton.label;"></html:a> +</hbox> + +<deck id="weavePrefsDeck" data-category="paneSync" hidden="true"> + <!-- These panels are for the "legacy" sync provider --> + <vbox id="noAccount" align="center"> + <spacer flex="1"/> + <description id="syncDesc"> + &weaveDesc.label; + </description> + <separator/> + <label id="noAccountSetup" class="text-link"> + &setupButton.label; + </label> + <vbox id="pairDevice"> + <separator/> + <label id="noAccountPair" class="text-link"> + &pairDevice.label; + </label> + </vbox> + <spacer flex="3"/> + </vbox> + + <vbox id="hasAccount"> + <groupbox class="syncGroupBox"> + <!-- label is set to account name --> + <caption id="accountCaption" align="center"> + <image id="accountCaptionImage"/> + <label id="accountName"/> + </caption> + + <hbox> + <button type="menu" + label="&manageAccount.label;" + accesskey="&manageAccount.accesskey;"> + <menupopup> + <menuitem id="syncChangePassword" label="&changePassword2.label;"/> + <menuitem id="syncResetPassphrase" label="&myRecoveryKey.label;"/> + <menuseparator/> + <menuitem id="syncReset" label="&resetSync2.label;"/> + </menupopup> + </button> + </hbox> + + <hbox> + <label id="syncAddDeviceLabel" + class="text-link"> + &pairDevice.label; + </label> + </hbox> + + <vbox> + <label>&syncMy.label;</label> + <richlistbox id="syncEnginesList" + orient="vertical"> + <richlistitem> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + preference="engine.addons"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + preference="engine.bookmarks"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.passwords.label;" + accesskey="&engine.passwords.accesskey;" + preference="engine.passwords"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.prefs.label;" + accesskey="&engine.prefs.accesskey;" + preference="engine.prefs"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + preference="engine.history"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.tabs.label;" + accesskey="&engine.tabs.accesskey;" + preference="engine.tabs"/> + </richlistitem> + </richlistbox> + </vbox> + </groupbox> + + <groupbox class="syncGroupBox"> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label control="syncComputerName"> + &syncDeviceName.label; + </label> + <textbox id="syncComputerName"/> + </row> + </rows> + </grid> + <hbox> + <label id="unlinkDevice" class="text-link"> + &unlinkDevice.label; + </label> + </hbox> + </groupbox> + <vbox id="tosPP-normal"> + <label id="tosPP-normal-ToS" class="text-link"> + &prefs.tosLink.label; + </label> + <label id="tosPP-normal-PP" class="text-link"> + &prefs.ppLink.label; + </label> + </vbox> + </vbox> + + <vbox id="needsUpdate" align="center" pack="center"> + <hbox> + <label id="loginError"/> + <label id="loginErrorUpdatePass" class="text-link"> + &updatePass.label; + </label> + <label id="loginErrorResetPass" class="text-link"> + &resetPass.label; + </label> + </hbox> + <label id="loginErrorStartOver" class="text-link"> + &unlinkDevice.label; + </label> + </vbox> + + <!-- These panels are for the Firefox Accounts identity provider --> + <vbox id="noFxaAccount"> + <hbox> + <vbox id="fxaContentWrapper"> + <groupbox id="noFxaGroup"> + <vbox> + <label id="noFxaCaption">&signedOut.caption;</label> + <description id="noFxaDescription" flex="1">&signedOut.description;</description> + <hbox class="fxaAccountBox"> + <vbox> + <image class="fxaFirefoxLogo"/> + </vbox> + <vbox flex="1"> + <label id="signedOutAccountBoxTitle">&signedOut.accountBox.title;</label> + <hbox class="fxaAccountBoxButtons"> + <button id="noFxaSignUp" label="&signedOut.accountBox.create;" accesskey="&signedOut.accountBox.create.accesskey;"></button> + <button id="noFxaSignIn" label="&signedOut.accountBox.signin;" accesskey="&signedOut.accountBox.signin.accesskey;"></button> + </hbox> + </vbox> + </hbox> + </vbox> + </groupbox> + </vbox> + <vbox> + <image class="fxaSyncIllustration"/> + </vbox> + </hbox> + <label class="fxaMobilePromo"> + &mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces + --><label id="fxaMobilePromo-android" + class="androidLink text-link"><!-- + -->&mobilePromo3.androidLink;</label><!-- + -->&mobilePromo3.iOSBefore;<!-- + --><label id="fxaMobilePromo-ios" + class="iOSLink text-link"><!-- + -->&mobilePromo3.iOSLink;</label><!-- + -->&mobilePromo3.end; + </label> + </vbox> + + <vbox id="hasFxaAccount"> + <hbox> + <vbox id="fxaContentWrapper"> + <groupbox id="fxaGroup"> + <caption><label>&syncBrand.fxAccount.label;</label></caption> + <deck id="fxaLoginStatus"> + + <!-- logged in and verified and all is good --> + <hbox id="fxaLoginVerified" class="fxaAccountBox"> + <vbox align="center" pack="center"> + <image id="fxaProfileImage" class="actionable" + role="button" + onclick="gSyncPane.openChangeProfileImage(event);" hidden="true" + onkeypress="gSyncPane.openChangeProfileImage(event);" + tooltiptext="&profilePicture.tooltip;"/> + </vbox> + <vbox flex="1" pack="center"> + <label id="fxaDisplayName" hidden="true"/> + <label id="fxaEmailAddress1"/> + <hbox class="fxaAccountBoxButtons"> + <button id="fxaUnlinkButton" label="&disconnect.label;" accesskey="&disconnect.accesskey;"/> + <html:a id="verifiedManage" target="_blank" + accesskey="&verifiedManage.accesskey;" + onkeypress="gSyncPane.openManageFirefoxAccount(event);"><!-- + -->&verifiedManage.label;</html:a> + </hbox> + </vbox> + </hbox> + + <!-- logged in to an unverified account --> + <hbox id="fxaLoginUnverified" class="fxaAccountBox"> + <vbox> + <image id="fxaProfileImage"/> + </vbox> + <vbox flex="1"> + <hbox> + <vbox><image id="fxaLoginRejectedWarning"/></vbox> + <description flex="1"> + &signedInUnverified.beforename.label; + <label id="fxaEmailAddress2"/> + &signedInUnverified.aftername.label; + </description> + </hbox> + <hbox class="fxaAccountBoxButtons"> + <button id="verifyFxaAccount" accesskey="&verify.accesskey;">&verify.label;</button> + <button id="unverifiedUnlinkFxaAccount" accesskey="&forget.accesskey;">&forget.label;</button> + </hbox> + </vbox> + </hbox> + + <!-- logged in locally but server rejected credentials --> + <hbox id="fxaLoginRejected" class="fxaAccountBox"> + <vbox> + <image id="fxaProfileImage"/> + </vbox> + <vbox flex="1"> + <hbox> + <vbox><image id="fxaLoginRejectedWarning"/></vbox> + <description flex="1"> + &signedInLoginFailure.beforename.label; + <label id="fxaEmailAddress3"/> + &signedInLoginFailure.aftername.label; + </description> + </hbox> + <hbox class="fxaAccountBoxButtons"> + <button id="rejectReSignIn" accessky="&signIn.accesskey;">&signIn.label;</button> + <button id="rejectUnlinkFxaAccount" accesskey="&forget.accesskey;">&forget.label;</button> + </hbox> + </vbox> + </hbox> + </deck> + </groupbox> + <groupbox id="syncOptions"> + <caption><label>&signedIn.engines.label;</label></caption> + <hbox id="fxaSyncEngines"> + <vbox align="start" flex="1"> + <checkbox label="&engine.tabs.label;" + accesskey="&engine.tabs.accesskey;" + preference="engine.tabs"/> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + preference="engine.bookmarks"/> + <checkbox label="&engine.passwords.label;" + accesskey="&engine.passwords.accesskey;" + preference="engine.passwords"/> + </vbox> + <vbox align="start" flex="1"> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + preference="engine.history"/> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + preference="engine.addons"/> + <checkbox label="&engine.prefs.label;" + accesskey="&engine.prefs.accesskey;" + preference="engine.prefs"/> + </vbox> + <spacer/> + </hbox> + </groupbox> + </vbox> + <vbox> + <image class="fxaSyncIllustration"/> + </vbox> + </hbox> + <groupbox> + <caption> + <label control="fxaSyncComputerName"> + &fxaSyncDeviceName.label; + </label> + </caption> + <hbox id="fxaDeviceName"> + <textbox id="fxaSyncComputerName" disabled="true"/> + <hbox> + <button id="fxaChangeDeviceName" + label="&changeSyncDeviceName.label;" + accesskey="&changeSyncDeviceName.accesskey;"/> + <button id="fxaCancelChangeDeviceName" + label="&cancelChangeSyncDeviceName.label;" + accesskey="&cancelChangeSyncDeviceName.accesskey;" + hidden="true"/> + <button id="fxaSaveChangeDeviceName" + label="&saveChangeSyncDeviceName.label;" + accesskey="&saveChangeSyncDeviceName.accesskey;" + hidden="true"/> + </hbox> + </hbox> + </groupbox> + <label class="fxaMobilePromo"> + &mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces + --><label class="androidLink text-link" id="fxaMobilePromo-android-hasFxaAccount"><!-- + -->&mobilePromo3.androidLink;</label><!-- + -->&mobilePromo3.iOSBefore;<!-- + --><label class="iOSLink text-link" id="fxaMobilePromo-ios-hasFxaAccount"><!-- + -->&mobilePromo3.iOSLink;</label><!-- + -->&mobilePromo3.end; + </label> + <vbox id="tosPP-small" align="start"> + <label id="tosPP-small-ToS" class="text-link"> + &prefs.tosLink.label; + </label> + <label id="tosPP-small-PP" class="text-link"> + &fxaPrivacyNotice.link.label; + </label> + </vbox> + </vbox> +</deck> diff --git a/browser/components/preferences/in-content/tests/.eslintrc.js b/browser/components/preferences/in-content/tests/.eslintrc.js new file mode 100644 index 000000000..7c8021192 --- /dev/null +++ b/browser/components/preferences/in-content/tests/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/browser/components/preferences/in-content/tests/browser.ini b/browser/components/preferences/in-content/tests/browser.ini new file mode 100644 index 000000000..6cba02599 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser.ini @@ -0,0 +1,43 @@ +[DEFAULT] +support-files = + head.js + privacypane_tests_perwindow.js + +[browser_advanced_update.js] +[browser_basic_rebuild_fonts_test.js] +[browser_bug410900.js] +[browser_bug705422.js] +[browser_bug731866.js] +[browser_bug795764_cachedisabled.js] +[browser_bug1018066_resetScrollPosition.js] +[browser_bug1020245_openPreferences_to_paneContent.js] +[browser_bug1184989_prevent_scrolling_when_preferences_flipped.js] +support-files = + browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul +[browser_change_app_handler.js] +skip-if = os != "win" # This test tests the windows-specific app selection dialog, so can't run on non-Windows +[browser_connection.js] +[browser_connection_bug388287.js] +[browser_cookies_exceptions.js] +[browser_defaultbrowser_alwayscheck.js] +[browser_healthreport.js] +skip-if = true || !healthreport # Bug 1185403 for the "true" +[browser_homepages_filter_aboutpreferences.js] +[browser_notifications_do_not_disturb.js] +[browser_permissions_urlFieldHidden.js] +[browser_proxy_backup.js] +[browser_privacypane_1.js] +[browser_privacypane_3.js] +[browser_privacypane_4.js] +[browser_privacypane_5.js] +[browser_privacypane_8.js] +[browser_sanitizeOnShutdown_prefLocked.js] +[browser_searchsuggestions.js] +[browser_security.js] +[browser_subdialogs.js] +support-files = + subdialog.xul + subdialog2.xul +[browser_telemetry.js] +# Skip this test on Android as FHR and Telemetry are separate systems there. +skip-if = !healthreport || !telemetry || (os == 'linux' && debug) || (os == 'android') diff --git a/browser/components/preferences/in-content/tests/browser_advanced_update.js b/browser/components/preferences/in-content/tests/browser_advanced_update.js new file mode 100644 index 000000000..e9d0e8652 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_advanced_update.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + +const mockUpdateManager = { + contractId: "@mozilla.org/updates/update-manager;1", + + _mockClassId: uuidGenerator.generateUUID(), + + _originalClassId: "", + + _originalFactory: null, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateManager]), + + createInstance: function(outer, iiD) { + if (outer) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(iiD); + }, + + register: function () { + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (!registrar.isCIDRegistered(this._mockClassId)) { + this._originalClassId = registrar.contractIDToCID(this.contractId); + this._originalFactory = Cm.getClassObject(Cc[this.contractId], Ci.nsIFactory); + registrar.unregisterFactory(this._originalClassId, this._originalFactory); + registrar.registerFactory(this._mockClassId, "Unregister after testing", this.contractId, this); + } + }, + + unregister: function () { + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(this._mockClassId, this); + registrar.registerFactory(this._originalClassId, "", this.contractId, this._originalFactory); + }, + + get updateCount() { + return this._updates.length; + }, + + getUpdateAt: function (index) { + return this._updates[index]; + }, + + _updates: [ + { + name: "Firefox Developer Edition 49.0a2", + statusText: "The Update was successfully installed", + buildID: "20160728004010", + type: "minor", + installDate: 1469763105156, + detailsURL: "https://www.mozilla.org/firefox/aurora/" + }, + { + name: "Firefox Developer Edition 43.0a2", + statusText: "The Update was successfully installed", + buildID: "20150929004011", + type: "minor", + installDate: 1443585886224, + detailsURL: "https://www.mozilla.org/firefox/aurora/" + }, + { + name: "Firefox Developer Edition 42.0a2", + statusText: "The Update was successfully installed", + buildID: "20150920004018", + type: "major", + installDate: 1442818147544, + detailsURL: "https://www.mozilla.org/firefox/aurora/" + } + ] +}; + +function resetPreferences() { + Services.prefs.clearUserPref("browser.search.update"); +} + +function formatInstallDate(sec) { + var date = new Date(sec); + const locale = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + const dtOptions = { year: 'numeric', month: 'long', day: 'numeric', + hour: 'numeric', minute: 'numeric', second: 'numeric' }; + return date.toLocaleString(locale, dtOptions); +} + +registerCleanupFunction(resetPreferences); + +add_task(function*() { + yield openPreferencesViaOpenPreferencesAPI("advanced", "updateTab", { leaveOpen: true }); + resetPreferences(); + Services.prefs.setBoolPref("browser.search.update", false); + + let doc = gBrowser.selectedBrowser.contentDocument; + let enableSearchUpdate = doc.getElementById("enableSearchUpdate"); + is_element_visible(enableSearchUpdate, "Check search update preference is visible"); + + // Ensure that the update pref dialog reflects the actual pref value. + ok(!enableSearchUpdate.checked, "Ensure search updates are disabled"); + Services.prefs.setBoolPref("browser.search.update", true); + ok(enableSearchUpdate.checked, "Ensure search updates are enabled"); + + gBrowser.removeCurrentTab(); +}); + +add_task(function*() { + mockUpdateManager.register(); + + yield openPreferencesViaOpenPreferencesAPI("advanced", "updateTab", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let showBtn = doc.getElementById("showUpdateHistory"); + let dialogOverlay = doc.getElementById("dialogOverlay"); + + // Test the dialog window opens + is(dialogOverlay.style.visibility, "", "The dialog should be invisible"); + showBtn.doCommand(); + yield promiseLoadSubDialog("chrome://mozapps/content/update/history.xul"); + is(dialogOverlay.style.visibility, "visible", "The dialog should be visible"); + + let dialogFrame = doc.getElementById("dialogFrame"); + let frameDoc = dialogFrame.contentDocument; + let updates = frameDoc.querySelectorAll("update"); + + // Test the update history numbers are correct + is(updates.length, mockUpdateManager.updateCount, "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); + + is(update.name, updateData.name + " (" + updateData.buildID + ")", "Wrong update name"); + is(update.type, updateData.type == "major" ? "New Version" : "Security Update", "Wrong update type"); + is(update.installDate, formatInstallDate(updateData.installDate), "Wrong update installDate"); + is(update.detailsURL, updateData.detailsURL, "Wrong update detailsURL"); + is(update.status, updateData.statusText, "Wrong update status"); + } + + // Test the dialog window closes + let closeBtn = doc.getElementById("dialogClose"); + closeBtn.doCommand(); + is(dialogOverlay.style.visibility, "", "The dialog should be invisible"); + + mockUpdateManager.unregister(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/in-content/tests/browser_basic_rebuild_fonts_test.js b/browser/components/preferences/in-content/tests/browser_basic_rebuild_fonts_test.js new file mode 100644 index 000000000..32c1bd726 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_basic_rebuild_fonts_test.js @@ -0,0 +1,76 @@ +Services.prefs.setBoolPref("browser.preferences.instantApply", true); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.preferences.instantApply"); +}); + +add_task(function*() { + yield openPreferencesViaOpenPreferencesAPI("paneContent", null, {leaveOpen: true}); + let doc = gBrowser.contentDocument; + var langGroup = Services.prefs.getComplexValue("font.language.group", Ci.nsIPrefLocalizedString).data + is(doc.getElementById("font.language.group").value, langGroup, + "Language group should be set correctly."); + + let defaultFontType = Services.prefs.getCharPref("font.default." + langGroup); + let fontFamily = Services.prefs.getCharPref("font.name." + defaultFontType + "." + langGroup); + let fontFamilyField = doc.getElementById("defaultFont"); + is(fontFamilyField.value, fontFamily, "Font family should be set correctly."); + + let defaultFontSize = Services.prefs.getIntPref("font.size.variable." + langGroup); + let fontSizeField = doc.getElementById("defaultFontSize"); + is(fontSizeField.value, defaultFontSize, "Font size should be set correctly."); + + doc.getElementById("advancedFonts").click(); + let win = yield promiseLoadSubDialog("chrome://browser/content/preferences/fonts.xul"); + doc = win.document; + + // Simulate a dumb font backend. + win.FontBuilder._enumerator = { + _list: ["MockedFont1", "MockedFont2", "MockedFont3"], + EnumerateFonts: function(lang, type, list) { + return this._list; + }, + EnumerateAllFonts: function() { + return this._list; + }, + getDefaultFont: function() { return null; }, + getStandardFamilyName: function(name) { return name; }, + }; + win.FontBuilder._allFonts = null; + win.FontBuilder._langGroupSupported = false; + + let langGroupElement = doc.getElementById("font.language.group"); + let selectLangsField = doc.getElementById("selectLangs"); + let serifField = doc.getElementById("serif"); + let armenian = "x-armn"; + let western = "x-western"; + + langGroupElement.value = armenian; + selectLangsField.value = armenian; + is(serifField.value, "", "Font family should not be set."); + + langGroupElement.value = western; + 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; + selectLangsField.value = armenian; + is(serifField.value, "MockedFont2", "Font family should be set."); + + langGroupElement.value = western; + selectLangsField.value = western; + + // Simulate a system that has no fonts for the specified language. + win.FontBuilder._enumerator._list = []; + + langGroupElement.value = armenian; + selectLangsField.value = armenian; + is(serifField.value, "", "Font family should not be set."); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/in-content/tests/browser_bug1018066_resetScrollPosition.js b/browser/components/preferences/in-content/tests/browser_bug1018066_resetScrollPosition.js new file mode 100644 index 000000000..9d938fdd4 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug1018066_resetScrollPosition.js @@ -0,0 +1,24 @@ +/* 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(function*() { + originalWindowHeight = window.outerHeight; + window.resizeTo(window.outerWidth, 300); + let prefs = yield openPreferencesViaOpenPreferencesAPI("paneApplications", undefined, {leaveOpen: true}); + is(prefs.selectedPane, "paneApplications", "Applications pane was selected"); + let mainContent = gBrowser.contentDocument.querySelector(".main-content"); + mainContent.scrollTop = 50; + is(mainContent.scrollTop, 50, "main-content should be scrolled 50 pixels"); + + gBrowser.contentWindow.gotoPref("paneGeneral"); + is(mainContent.scrollTop, 0, + "Switching to a different category should reset the scroll position"); +}); + diff --git a/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js b/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js new file mode 100644 index 000000000..bc2c6d800 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Services.prefs.setBoolPref("browser.preferences.instantApply", true); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.preferences.instantApply"); +}); + +add_task(function*() { + let prefs = yield openPreferencesViaOpenPreferencesAPI("paneContent"); + is(prefs.selectedPane, "paneContent", "Content pane was selected"); + prefs = yield openPreferencesViaOpenPreferencesAPI("advanced", "updateTab"); + is(prefs.selectedPane, "paneAdvanced", "Advanced pane was selected"); + is(prefs.selectedAdvancedTab, "updateTab", "The update tab within the advanced prefs should be selected"); + prefs = yield openPreferencesViaHash("privacy"); + is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected when hash is 'privacy'"); + prefs = yield openPreferencesViaOpenPreferencesAPI("nonexistant-category"); + is(prefs.selectedPane, "paneGeneral", "General pane is selected by default when a nonexistant-category is requested"); + prefs = yield openPreferencesViaHash("nonexistant-category"); + is(prefs.selectedPane, "paneGeneral", "General pane is selected when hash is a nonexistant-category"); + prefs = yield openPreferencesViaHash(); + is(prefs.selectedPane, "paneGeneral", "General pane is selected by default"); +}); + +function openPreferencesViaHash(aPane) { + let deferred = Promise.defer(); + gBrowser.selectedTab = gBrowser.addTab("about:preferences" + (aPane ? "#" + aPane : "")); + let newTabBrowser = gBrowser.selectedBrowser; + + newTabBrowser.addEventListener("Initialized", function PrefInit() { + newTabBrowser.removeEventListener("Initialized", PrefInit, true); + newTabBrowser.contentWindow.addEventListener("load", function prefLoad() { + newTabBrowser.contentWindow.removeEventListener("load", prefLoad); + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + gBrowser.removeCurrentTab(); + deferred.resolve({selectedPane: selectedPane}); + }); + }, true); + + return deferred.promise; +} diff --git a/browser/components/preferences/in-content/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js b/browser/components/preferences/in-content/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js new file mode 100644 index 000000000..0972b2de4 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js @@ -0,0 +1,92 @@ +const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + +add_task(function* () { + waitForExplicitFinish(); + + const tabURL = getRootDirectory(gTestPath) + "browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul"; + + yield BrowserTestUtils.withNewTab({ gBrowser, url: tabURL }, function* (browser) { + let doc = browser.contentDocument; + let container = doc.getElementById("container"); + + // Test button + let button = doc.getElementById("button"); + button.focus(); + EventUtils.synthesizeKey(" ", {}); + yield checkPageScrolling(container, "button"); + + // Test checkbox + let checkbox = doc.getElementById("checkbox"); + checkbox.focus(); + EventUtils.synthesizeKey(" ", {}); + ok(checkbox.checked, "Checkbox is checked"); + yield checkPageScrolling(container, "checkbox"); + + // Test listbox + let listbox = doc.getElementById("listbox"); + let listitem = doc.getElementById("listitem"); + listbox.focus(); + EventUtils.synthesizeKey(" ", {}); + ok(listitem.selected, "Listitem is selected"); + yield checkPageScrolling(container, "listbox"); + + // Test radio + let radiogroup = doc.getElementById("radiogroup"); + radiogroup.focus(); + EventUtils.synthesizeKey(" ", {}); + yield checkPageScrolling(container, "radio"); + }); + + yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:preferences#search" }, function* (browser) { + let doc = browser.contentDocument; + let container = doc.getElementsByClassName("main-content")[0]; + + // Test search + let engineList = doc.getElementById("engineList"); + engineList.focus(); + EventUtils.synthesizeKey(" ", {}); + is(engineList.view.selection.currentIndex, 0, "Search engineList is selected"); + EventUtils.synthesizeKey(" ", {}); + yield checkPageScrolling(container, "search engineList"); + }); + + // Test session restore + const CRASH_URL = "about:mozilla"; + const CRASH_FAVICON = "chrome://branding/content/icon32.png"; + const CRASH_SHENTRY = {url: CRASH_URL}; + const CRASH_TAB = {entries: [CRASH_SHENTRY], image: CRASH_FAVICON}; + const CRASH_STATE = {windows: [{tabs: [CRASH_TAB]}]}; + + const TAB_URL = "about:sessionrestore"; + const TAB_FORMDATA = {url: TAB_URL, id: {sessionData: CRASH_STATE}}; + const TAB_SHENTRY = {url: TAB_URL}; + const TAB_STATE = {entries: [TAB_SHENTRY], formdata: TAB_FORMDATA}; + + let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + // Fake a post-crash tab + ss.setTabState(tab, JSON.stringify(TAB_STATE)); + + yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let doc = tab.linkedBrowser.contentDocument; + + // Make body scrollable + doc.body.style.height = (doc.body.clientHeight + 100) + "px"; + + let tabList = doc.getElementById("tabList"); + tabList.focus(); + EventUtils.synthesizeKey(" ", {}); + yield checkPageScrolling(doc.documentElement, "session restore"); + + gBrowser.removeCurrentTab(); + finish(); +}); + +function checkPageScrolling(container, type) { + return new Promise(resolve => { + setTimeout(() => { + is(container.scrollTop, 0, "Page should not scroll when " + type + " flipped"); + resolve(); + }, 0); + }); +} diff --git a/browser/components/preferences/in-content/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul b/browser/components/preferences/in-content/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul new file mode 100644 index 000000000..59b644c8f --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<!-- + XUL Widget Test for Bug 1184989 + --> +<page 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 style="height: 50px;"> + <listbox id="listbox"> + <listitem id="listitem" label="listitem" /> + <listitem label="listitem" /> + </listbox> + </hbox> + + <hbox> + <radiogroup id="radiogroup"> + <radio id="radio" label="radio" /> + </radiogroup> + </hbox> + </vbox> +</vbox> + +</page> diff --git a/browser/components/preferences/in-content/tests/browser_bug410900.js b/browser/components/preferences/in-content/tests/browser_bug410900.js new file mode 100644 index 000000000..5b100966d --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug410900.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +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, false); + + var hserv = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + hserv.store(info); + + openPreferencesViaOpenPreferencesAPI("applications", null, {leaveOpen: true}).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 > 0, "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/in-content/tests/browser_bug705422.js b/browser/components/preferences/in-content/tests/browser_bug705422.js new file mode 100644 index 000000000..24732083b --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug705422.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + // Allow all cookies, then actually set up the test + SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0]]}, initTest); +} + +function initTest() { + const searchTerm = "example"; + const dummyTerm = "elpmaxe"; + + var cm = Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Components.interfaces.nsICookieManager); + + // delete all cookies (might be left over from other tests) + cm.removeAll(); + + // data for cookies + var vals = [[searchTerm+".com", dummyTerm, dummyTerm], // match + [searchTerm+".org", dummyTerm, dummyTerm], // match + [dummyTerm+".com", searchTerm, dummyTerm], // match + [dummyTerm+".edu", searchTerm+dummyTerm, dummyTerm], // match + [dummyTerm+".net", dummyTerm, searchTerm], // match + [dummyTerm+".org", dummyTerm, searchTerm+dummyTerm], // match + [dummyTerm+".int", dummyTerm, dummyTerm]]; // no match + + // matches must correspond to above data + const matches = 6; + + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var cookieSvc = Components.classes["@mozilla.org/cookieService;1"] + .getService(Components.interfaces.nsICookieService); + var v; + // inject cookies + for (v in vals) { + let [host, name, value] = vals[v]; + var cookieUri = ios.newURI("http://"+host, null, null); + cookieSvc.setCookieString(cookieUri, null, name+"="+value+";", null); + } + + // open cookie manager + var cmd = window.openDialog("chrome://browser/content/preferences/cookies.xul", + "Browser:Cookies", "", {}); + + // when it has loaded, run actual tests + cmd.addEventListener("load", function() { executeSoon(function() { runTest(cmd, searchTerm, vals.length, matches); }); }, false); +} + +function isDisabled(win, expectation) { + var disabled = win.document.getElementById("removeAllCookies").disabled; + is(disabled, expectation, "Remove all cookies button has correct state: "+(expectation?"disabled":"enabled")); +} + +function runTest(win, searchTerm, cookies, matches) { + var cm = Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Components.interfaces.nsICookieManager); + + + // number of cookies should match injected cookies + var injectedCookies = 0, + injectedEnumerator = cm.enumerator; + while (injectedEnumerator.hasMoreElements()) { + injectedCookies++; + injectedEnumerator.getNext(); + } + is(injectedCookies, cookies, "Number of cookies match injected cookies"); + + // "delete all cookies" should be enabled + isDisabled(win, false); + + // filter cookies and count matches + win.gCookiesWindow.setFilter(searchTerm); + is(win.gCookiesWindow._view.rowCount, matches, "Correct number of cookies shown after filter is applied"); + + // "delete all cookies" should be enabled + isDisabled(win, false); + + + // select first cookie and delete + var tree = win.document.getElementById("cookiesList"); + var deleteButton = win.document.getElementById("removeSelectedCookies"); + var rect = tree.treeBoxObject.getCoordsForCellItem(0, tree.columns[0], "cell"); + EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, {}, win); + EventUtils.synthesizeMouseAtCenter(deleteButton, {}, win); + + // count cookies should be matches-1 + is(win.gCookiesWindow._view.rowCount, matches-1, "Deleted selected cookie"); + + // select two adjacent cells and delete + EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, {}, win); + var eventObj = {}; + if (navigator.platform.indexOf("Mac") >= 0) + eventObj.metaKey = true; + else + eventObj.ctrlKey = true; + rect = tree.treeBoxObject.getCoordsForCellItem(1, tree.columns[0], "cell"); + EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, eventObj, win); + EventUtils.synthesizeMouseAtCenter(deleteButton, {}, win); + + // count cookies should be matches-3 + is(win.gCookiesWindow._view.rowCount, matches-3, "Deleted selected two adjacent cookies"); + + // "delete all cookies" should be enabled + isDisabled(win, false); + + // delete all cookies and count + var deleteAllButton = win.document.getElementById("removeAllCookies"); + EventUtils.synthesizeMouseAtCenter(deleteAllButton, {}, win); + is(win.gCookiesWindow._view.rowCount, 0, "Deleted all matching cookies"); + + // "delete all cookies" should be disabled + isDisabled(win, true); + + // clear filter and count should be cookies-matches + win.gCookiesWindow.setFilter(""); + is(win.gCookiesWindow._view.rowCount, cookies-matches, "Unmatched cookies remain"); + + // "delete all cookies" should be enabled + isDisabled(win, false); + + // delete all cookies and count should be 0 + EventUtils.synthesizeMouseAtCenter(deleteAllButton, {}, win); + is(win.gCookiesWindow._view.rowCount, 0, "Deleted all cookies"); + + // check that datastore is also at 0 + var remainingCookies = 0, + remainingEnumerator = cm.enumerator; + while (remainingEnumerator.hasMoreElements()) { + remainingCookies++; + remainingEnumerator.getNext(); + } + is(remainingCookies, 0, "Zero cookies remain"); + + // "delete all cookies" should be disabled + isDisabled(win, true); + + // clean up + win.close(); + finish(); +} + diff --git a/browser/components/preferences/in-content/tests/browser_bug731866.js b/browser/components/preferences/in-content/tests/browser_bug731866.js new file mode 100644 index 000000000..c1031d412 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug731866.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +function test() { + waitForExplicitFinish(); + open_preferences(runTest); +} + +var gElements; + +function checkElements(expectedPane) { + for (let element of gElements) { + // keyset and preferences 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.nodeName == "preferences" || + element.id === "drmGroup") { + 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); + } + } +} + +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", "Content", "Applications", + "Privacy", "Security", "Sync", "Advanced", + ]; + + for (let pane of panes) { + win.gotoPref("pane" + pane); + checkElements(pane); + } + + gBrowser.removeCurrentTab(); + win.close(); + finish(); +} diff --git a/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js new file mode 100644 index 000000000..21f92db8d --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +function test() { + waitForExplicitFinish(); + + let prefs = [ + "browser.cache.offline.enable", + "browser.cache.disk.enable", + "browser.cache.memory.enable", + ]; + + registerCleanupFunction(function() { + for (let pref of prefs) { + Services.prefs.clearUserPref(pref); + } + }); + + for (let pref of prefs) { + Services.prefs.setBoolPref(pref, false); + } + + open_preferences(runTest); +} + +function runTest(win) { + is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded"); + + let tab = win.document; + let elements = tab.getElementById("mainPrefPane").children; + + // Test if advanced pane is opened correctly + win.gotoPref("paneAdvanced"); + for (let element of elements) { + if (element.nodeName == "preferences") { + continue; + } + let attributeValue = element.getAttribute("data-category"); + if (attributeValue == "paneAdvanced") { + is_element_visible(element, "Advanced elements should be visible"); + } else { + is_element_hidden(element, "Non-Advanced elements should be hidden"); + } + } + + gBrowser.removeCurrentTab(); + win.close(); + finish(); +} diff --git a/browser/components/preferences/in-content/tests/browser_change_app_handler.js b/browser/components/preferences/in-content/tests/browser_change_app_handler.js new file mode 100644 index 000000000..f66cdfd37 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_change_app_handler.js @@ -0,0 +1,98 @@ +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, false); + + gHandlerSvc.store(infoToModify); +} + +add_task(function*() { + setupFakeHandler(); + yield openPreferencesViaOpenPreferencesAPI("applications", null, {leaveOpen: true}); + info("Preferences page opened on the applications pane."); + 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 = yield waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu")); + info("Got list after item was selected"); + + let chooseItem = list.firstChild.querySelector(".choose-app-item"); + let dialogLoadedPromise = promiseLoadSubDialog("chrome://global/content/appPicker.xul"); + let cmdEvent = win.document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null); + chooseItem.dispatchEvent(cmdEvent); + + let dialog = yield dialogLoadedPromise; + info("Dialog loaded"); + + let dialogDoc = dialog.document; + let dialogList = dialogDoc.getElementById("app-picker-listbox"); + dialogList.selectItem(dialogList.firstChild); + let selectedApp = dialogList.firstChild.handlerApp; + dialogDoc.documentElement.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: + list = yield waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu")); + info("Got list after item was selected"); + 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/applicationManager.xul"); + + let manageItem = list.firstChild.querySelector(".manage-app-item"); + cmdEvent = win.document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null); + manageItem.dispatchEvent(cmdEvent); + + dialog = yield dialogLoadedPromise; + info("Dialog loaded the second time"); + + dialogDoc = dialog.document; + dialogList = dialogDoc.getElementById("appList"); + let itemToRemove = dialogList.querySelector('listitem[label="' + selectedApp.name + '"]'); + 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"); + dialogDoc.documentElement.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: + list = yield waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu")); + ok(list.selectedItem, "Should have a selected item"); + ok(!list.selectedItem.handlerApp, + "No app should be visible as preferred item."); + + gBrowser.removeCurrentTab(); +}); + +registerCleanupFunction(function() { + let infoToModify = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null); + gHandlerSvc.remove(infoToModify); +}); + diff --git a/browser/components/preferences/in-content/tests/browser_connection.js b/browser/components/preferences/in-content/tests/browser_connection.js new file mode 100644 index 000000000..50438aed1 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_connection.js @@ -0,0 +1,99 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); + +function test() { + waitForExplicitFinish(); + + // network.proxy.type needs to be backed up and restored because mochitest + // changes this setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function() { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.no_proxies_on"); + Services.prefs.clearUserPref("browser.preferences.instantApply"); + }); + + let connectionURL = "chrome://browser/content/preferences/connection.xul"; + + /* + 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(Task.async(function* tabOpened(aContentWindow) { + is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded"); + let dialog = yield openAndLoadSubDialog(connectionURL); + let dialogClosingPromise = waitForEvent(dialog.document.documentElement, "dialogclosing"); + + ok(dialog, "connection window opened"); + runConnectionTests(dialog); + dialog.document.documentElement.acceptDialog(); + + let dialogClosingEvent = yield 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 = doc.getElementById("network.proxy.no_proxies_on"); + let networkProxyTypePref = doc.getElementById("network.proxy.type"); + + // make sure the networkProxyNone textbox is formatted properly + is(networkProxyNone.getAttribute("multiline"), "true", + "networkProxyNone textbox is multiline"); + is(networkProxyNone.getAttribute("rows"), "2", + "networkProxyNone textbox has two rows"); + + // 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/in-content/tests/browser_connection_bug388287.js b/browser/components/preferences/in-content/tests/browser_connection_bug388287.js new file mode 100644 index 000000000..5a348876e --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_connection_bug388287.js @@ -0,0 +1,125 @@ +/* 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/ */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); + +function test() { + waitForExplicitFinish(); + const connectionURL = "chrome://browser/content/preferences/connection.xul"; + let closeable = false; + let finalTest = false; + + // The changed preferences need to be backed up and restored because this mochitest + // changes them setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function() { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + for (let proxyType of ["http", "ssl", "ftp", "socks"]) { + Services.prefs.clearUserPref("network.proxy." + proxyType); + Services.prefs.clearUserPref("network.proxy." + proxyType + "_port"); + if (proxyType == "http") { + continue; + } + Services.prefs.clearUserPref("network.proxy.backup." + proxyType); + Services.prefs.clearUserPref("network.proxy.backup." + proxyType + "_port"); + } + }); + + /* + The connection dialog alone won't save onaccept since it uses type="child", + so it has to be opened as a sub dialog of the main pref tab. + Open the main tab here. + */ + open_preferences(Task.async(function* tabOpened(aContentWindow) { + let dialog, dialogClosingPromise; + let doc, proxyTypePref, sharePref, httpPref, httpPortPref, ftpPref, ftpPortPref; + + // Convenient function to reset the variables for the new window + function* setDoc() { + if (closeable) { + let dialogClosingEvent = yield dialogClosingPromise; + ok(dialogClosingEvent, "Connection dialog closed"); + } + + if (finalTest) { + gBrowser.removeCurrentTab(); + finish(); + return; + } + + dialog = yield openAndLoadSubDialog(connectionURL); + dialogClosingPromise = waitForEvent(dialog.document.documentElement, "dialogclosing"); + + doc = dialog.document; + proxyTypePref = doc.getElementById("network.proxy.type"); + sharePref = doc.getElementById("network.proxy.share_proxy_settings"); + httpPref = doc.getElementById("network.proxy.http"); + httpPortPref = doc.getElementById("network.proxy.http_port"); + ftpPref = doc.getElementById("network.proxy.ftp"); + ftpPortPref = doc.getElementById("network.proxy.ftp_port"); + } + + // This batch of tests should not close the dialog + yield setDoc(); + + // Testing HTTP port 0 with share on + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = "localhost"; + httpPortPref.value = 0; + doc.documentElement.acceptDialog(); + + // Testing HTTP port 0 + FTP port 80 with share off + sharePref.value = false; + ftpPref.value = "localhost"; + ftpPortPref.value = 80; + doc.documentElement.acceptDialog(); + + // Testing HTTP port 80 + FTP port 0 with share off + httpPortPref.value = 80; + ftpPortPref.value = 0; + doc.documentElement.acceptDialog(); + + // From now on, the dialog should close since we are giving it legitimate inputs. + // The test will timeout if the onbeforeaccept kicks in erroneously. + closeable = true; + + // Both ports 80, share on + httpPortPref.value = 80; + ftpPortPref.value = 80; + doc.documentElement.acceptDialog(); + + // HTTP 80, FTP 0, with share on + yield setDoc(); + proxyTypePref.value = 1; + sharePref.value = true; + ftpPref.value = "localhost"; + httpPref.value = "localhost"; + httpPortPref.value = 80; + ftpPortPref.value = 0; + doc.documentElement.acceptDialog(); + + // HTTP host empty, port 0 with share on + yield setDoc(); + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = ""; + httpPortPref.value = 0; + doc.documentElement.acceptDialog(); + + // HTTP 0, but in no proxy mode + yield 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; + doc.documentElement.acceptDialog(); + yield setDoc(); + })); +} diff --git a/browser/components/preferences/in-content/tests/browser_cookies_exceptions.js b/browser/components/preferences/in-content/tests/browser_cookies_exceptions.js new file mode 100644 index 000000000..89313d736 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_cookies_exceptions.js @@ -0,0 +1,348 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +requestLongerTimeout(2); + +function test() { + waitForExplicitFinish(); + requestLongerTimeout(3); + testRunner.runTests(); +} + +var testRunner = { + + tests: + [ + { + test: function(params) { + params.url.value = "test.com"; + params.btnAllow.doCommand(); + is(params.tree.view.rowCount, 1, "added exception shows up in treeview"); + is(params.tree.view.getCellText(0, params.nameCol), "http://test.com", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.allowText, + "permission text should be set correctly"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://test.com", data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION }], + }, + { + test: function(params) { + params.url.value = "test.com"; + params.btnBlock.doCommand(); + is(params.tree.view.getCellText(0, params.nameCol), "http://test.com", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.denyText, + "permission should change to deny in UI"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://test.com", data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION }], + }, + { + test: function(params) { + params.url.value = "test.com"; + params.btnAllow.doCommand(); + is(params.tree.view.getCellText(0, params.nameCol), "http://test.com", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.allowText, + "permission should revert back to allow"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://test.com", data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION }], + }, + { + test: function(params) { + params.url.value = "test.com"; + params.btnRemove.doCommand(); + is(params.tree.view.rowCount, 0, "exception should be removed"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://test.com", data: "deleted" }], + }, + { + expectPermObservancesDuringTestFunction: true, + test: function(params) { + let uri = params.ioService.newURI("http://test.com", null, null); + params.pm.add(uri, "popup", Ci.nsIPermissionManager.DENY_ACTION); + is(params.tree.view.rowCount, 0, "adding unrelated permission should not change display"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "popup", origin: "http://test.com", data: "added", + capability: Ci.nsIPermissionManager.DENY_ACTION }], + cleanUp: function(params) { + let uri = params.ioService.newURI("http://test.com", null, null); + params.pm.remove(uri, "popup"); + }, + }, + { + test: function(params) { + params.url.value = "https://test.com:12345"; + params.btnAllow.doCommand(); + is(params.tree.view.rowCount, 1, "added exception shows up in treeview"); + is(params.tree.view.getCellText(0, params.nameCol), "https://test.com:12345", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.allowText, + "permission text should be set correctly"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "https://test.com:12345", data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION }], + }, + { + test: function(params) { + params.url.value = "https://test.com:12345"; + params.btnBlock.doCommand(); + is(params.tree.view.getCellText(0, params.nameCol), "https://test.com:12345", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.denyText, + "permission should change to deny in UI"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "https://test.com:12345", data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION }], + }, + { + test: function(params) { + params.url.value = "https://test.com:12345"; + params.btnAllow.doCommand(); + is(params.tree.view.getCellText(0, params.nameCol), "https://test.com:12345", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.allowText, + "permission should revert back to allow"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "https://test.com:12345", data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION }], + }, + { + test: function(params) { + params.url.value = "https://test.com:12345"; + params.btnRemove.doCommand(); + is(params.tree.view.rowCount, 0, "exception should be removed"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "https://test.com:12345", data: "deleted" }], + }, + { + test: function(params) { + params.url.value = "localhost:12345"; + params.btnAllow.doCommand(); + is(params.tree.view.rowCount, 1, "added exception shows up in treeview"); + is(params.tree.view.getCellText(0, params.nameCol), "http://localhost:12345", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.allowText, + "permission text should be set correctly"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://localhost:12345", data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION }], + }, + { + test: function(params) { + params.url.value = "localhost:12345"; + params.btnBlock.doCommand(); + is(params.tree.view.getCellText(0, params.nameCol), "http://localhost:12345", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.denyText, + "permission should change to deny in UI"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://localhost:12345", data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION }], + }, + { + test: function(params) { + params.url.value = "localhost:12345"; + params.btnAllow.doCommand(); + is(params.tree.view.getCellText(0, params.nameCol), "http://localhost:12345", + "origin name should be set correctly"); + is(params.tree.view.getCellText(0, params.statusCol), params.allowText, + "permission should revert back to allow"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://localhost:12345", data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION }], + }, + { + test: function(params) { + params.url.value = "localhost:12345"; + params.btnRemove.doCommand(); + is(params.tree.view.rowCount, 0, "exception should be removed"); + params.btnApplyChanges.doCommand(); + }, + observances: [{ type: "cookie", origin: "http://localhost:12345", data: "deleted" }], + }, + { + expectPermObservancesDuringTestFunction: true, + test(params) { + for (let URL of ["http://a", "http://z", "http://b"]) { + let URI = params.ioService.newURI(URL, null, null); + params.pm.add(URI, "cookie", Ci.nsIPermissionManager.ALLOW_ACTION); + } + + is(params.tree.view.rowCount, 3, "Three permissions should be present"); + is(params.tree.view.getCellText(0, params.nameCol), "http://a", + "site should be sorted. 'a' should be first"); + is(params.tree.view.getCellText(1, params.nameCol), "http://b", + "site should be sorted. 'b' should be second"); + is(params.tree.view.getCellText(2, params.nameCol), "http://z", + "site should be sorted. 'z' should be third"); + + // Sort descending then check results in cleanup since sorting isn't synchronous. + EventUtils.synthesizeMouseAtCenter(params.doc.getElementById("siteCol"), {}, + params.doc.defaultView); + params.btnApplyChanges.doCommand(); + }, + observances: [{ 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 }], + cleanUp(params) { + is(params.tree.view.getCellText(0, params.nameCol), "http://z", + "site should be sorted. 'z' should be first"); + is(params.tree.view.getCellText(1, params.nameCol), "http://b", + "site should be sorted. 'b' should be second"); + is(params.tree.view.getCellText(2, params.nameCol), "http://a", + "site should be sorted. 'a' should be third"); + + for (let URL of ["http://a", "http://z", "http://b"]) { + let uri = params.ioService.newURI(URL, null, null); + params.pm.remove(uri, "cookie"); + } + }, + }, + ], + + _currentTest: -1, + + runTests: function() { + this._currentTest++; + + info("Running test #" + (this._currentTest + 1) + "\n"); + let that = this; + let p = this.runCurrentTest(this._currentTest + 1); + p.then(function() { + if (that._currentTest == that.tests.length - 1) { + finish(); + } + else { + that.runTests(); + } + }); + }, + + runCurrentTest: function(testNumber) { + return new Promise(function(resolve, reject) { + + let helperFunctions = { + windowLoad: function(win) { + let doc = win.document; + let params = { + doc, + tree: doc.getElementById("permissionsTree"), + nameCol: doc.getElementById("permissionsTree").treeBoxObject.columns.getColumnAt(0), + statusCol: doc.getElementById("permissionsTree").treeBoxObject.columns.getColumnAt(1), + url: doc.getElementById("url"), + btnAllow: doc.getElementById("btnAllow"), + btnBlock: doc.getElementById("btnBlock"), + btnApplyChanges: doc.getElementById("btnApplyChanges"), + btnRemove: doc.getElementById("removePermission"), + pm: Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager), + ioService: Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService), + allowText: win.gPermissionManager._getCapabilityString( + Ci.nsIPermissionManager.ALLOW_ACTION), + denyText: win.gPermissionManager._getCapabilityString( + Ci.nsIPermissionManager.DENY_ACTION), + allow: Ci.nsIPermissionManager.ALLOW_ACTION, + deny: Ci.nsIPermissionManager.DENY_ACTION, + }; + + let permObserver = { + observe: function(aSubject, aTopic, aData) { + if (aTopic != "perm-changed") + return; + + if (testRunner.tests[testRunner._currentTest].observances.length == 0) { + // Should fail here as we are not expecting a notification, but we don't. + // See bug 1063410. + return; + } + + let permission = aSubject.QueryInterface(Ci.nsIPermission); + let expected = testRunner.tests[testRunner._currentTest].observances.shift(); + + is(aData, expected.data, "type of message should be the same"); + for (let prop of ["type", "capability"]) { + if (expected[prop]) + is(permission[prop], expected[prop], + "property: \"" + prop + "\" should be equal"); + } + + if (expected.origin) { + is(permission.principal.origin, expected.origin, + "property: \"origin\" should be equal"); + } + + os.removeObserver(permObserver, "perm-changed"); + + let test = testRunner.tests[testRunner._currentTest]; + if (!test.expectPermObservancesDuringTestFunction) { + if (test.cleanUp) { + test.cleanUp(params); + } + + gBrowser.removeCurrentTab(); + resolve(); + } + }, + }; + + let os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + + os.addObserver(permObserver, "perm-changed", false); + + if (testRunner._currentTest == 0) { + is(params.tree.view.rowCount, 0, "no cookie exceptions"); + } + + try { + let test = testRunner.tests[testRunner._currentTest]; + test.test(params); + if (test.expectPermObservancesDuringTestFunction) { + if (test.cleanUp) { + test.cleanUp(params); + } + + gBrowser.removeCurrentTab(); + resolve(); + } + } catch (ex) { + ok(false, "exception while running test #" + + testNumber + ": " + ex); + } + }, + }; + + openPreferencesViaOpenPreferencesAPI("panePrivacy", null, {leaveOpen: true}).then(function() { + let doc = gBrowser.contentDocument; + let historyMode = doc.getElementById("historyMode"); + historyMode.value = "custom"; + historyMode.doCommand(); + doc.getElementById("cookieExceptions").doCommand(); + + let subDialogURL = "chrome://browser/content/preferences/permissions.xul"; + promiseLoadSubDialog(subDialogURL).then(function(win) { + helperFunctions.windowLoad(win); + }); + }); + }); + }, +}; diff --git a/browser/components/preferences/in-content/tests/browser_defaultbrowser_alwayscheck.js b/browser/components/preferences/in-content/tests/browser_defaultbrowser_alwayscheck.js new file mode 100644 index 000000000..b30b6d9e2 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_defaultbrowser_alwayscheck.js @@ -0,0 +1,103 @@ +"use strict"; + +const CHECK_DEFAULT_INITIAL = Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"); + +add_task(function* clicking_make_default_checks_alwaysCheck_checkbox() { + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + yield test_with_mock_shellservice({isDefault: false}, function*() { + let setDefaultPane = content.document.getElementById("setDefaultPane"); + Assert.equal(setDefaultPane.selectedIndex, "0", + "The 'make default' pane should be visible when not default"); + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok(!alwaysCheck.checked, "Always Check is unchecked by default"); + Assert.ok(!Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "alwaysCheck pref should be false by default in test runs"); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + yield ContentTaskUtils.waitForCondition(() => alwaysCheck.checked, + "'Always Check' checkbox should get checked after clicking the 'Set Default' button"); + + Assert.ok(alwaysCheck.checked, + "Clicking 'Make Default' checks the 'Always Check' checkbox"); + Assert.ok(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "Checking the checkbox should set the pref to true"); + Assert.ok(alwaysCheck.disabled, + "'Always Check' checkbox is locked with default browser and alwaysCheck=true"); + Assert.equal(setDefaultPane.selectedIndex, "1", + "The 'make default' pane should not be visible when default"); + Assert.ok(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "checkDefaultBrowser pref is now enabled"); + }); + + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser"); +}); + +add_task(function* clicking_make_default_checks_alwaysCheck_checkbox() { + Services.prefs.lockPref("browser.shell.checkDefaultBrowser"); + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + yield test_with_mock_shellservice({isDefault: false}, function*() { + let setDefaultPane = content.document.getElementById("setDefaultPane"); + Assert.equal(setDefaultPane.selectedIndex, "0", + "The 'make default' pane should be visible when not default"); + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok(alwaysCheck.disabled, "Always Check is disabled when locked"); + Assert.ok(alwaysCheck.checked, + "Always Check is checked because defaultPref is true and pref is locked"); + Assert.ok(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "alwaysCheck pref should ship with 'true' by default"); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + yield ContentTaskUtils.waitForCondition(() => setDefaultPane.selectedIndex == "1", + "Browser is now default"); + + Assert.ok(alwaysCheck.checked, + "'Always Check' is still checked because it's locked"); + Assert.ok(alwaysCheck.disabled, + "'Always Check is disabled because it's locked"); + Assert.ok(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "The pref is locked and so doesn't get changed"); + }); + + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + gBrowser.removeCurrentTab(); +}); + +registerCleanupFunction(function() { + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", CHECK_DEFAULT_INITIAL); +}); + +function* test_with_mock_shellservice(options, testFn) { + yield ContentTask.spawn(gBrowser.selectedBrowser, options, function*(options) { + 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 = options.isDefault; + win.gMainPane.updateSetDefaultBrowser(); + }); + + yield ContentTask.spawn(gBrowser.selectedBrowser, null, testFn); + + Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", CHECK_DEFAULT_INITIAL); +} diff --git a/browser/components/preferences/in-content/tests/browser_healthreport.js b/browser/components/preferences/in-content/tests/browser_healthreport.js new file mode 100644 index 000000000..bbfae9707 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_healthreport.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. +* http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +function runPaneTest(fn) { + open_preferences((win) => { + let doc = win.document; + win.gotoPref("paneAdvanced"); + let advancedPrefs = doc.getElementById("advancedPrefs"); + let tab = doc.getElementById("dataChoicesTab"); + advancedPrefs.selectedTab = tab; + fn(win, doc); + }); +} + +function test() { + waitForExplicitFinish(); + resetPreferences(); + registerCleanupFunction(resetPreferences); + runPaneTest(testBasic); +} + +function testBasic(win, doc) { + is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), true, + "Health Report upload enabled on app first run."); + + let checkbox = doc.getElementById("submitHealthReportBox"); + ok(checkbox); + is(checkbox.checked, true, "Health Report checkbox is checked on app first run."); + + checkbox.checked = false; + checkbox.doCommand(); + is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), false, + "Unchecking checkbox opts out of FHR upload."); + + checkbox.checked = true; + checkbox.doCommand(); + is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), true, + "Checking checkbox allows FHR upload."); + + win.close(); + Services.prefs.lockPref(FHR_UPLOAD_ENABLED); + runPaneTest(testUploadDisabled); +} + +function testUploadDisabled(win, doc) { + ok(Services.prefs.prefIsLocked(FHR_UPLOAD_ENABLED), "Upload enabled flag is locked."); + let checkbox = doc.getElementById("submitHealthReportBox"); + is(checkbox.getAttribute("disabled"), "true", "Checkbox is disabled if upload flag is locked."); + Services.prefs.unlockPref(FHR_UPLOAD_ENABLED); + + win.close(); + finish(); +} + +function resetPreferences() { + Services.prefs.clearUserPref(FHR_UPLOAD_ENABLED); +} + diff --git a/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js b/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js new file mode 100644 index 000000000..366454fcc --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js @@ -0,0 +1,20 @@ +add_task(function*() { + is(gBrowser.currentURI.spec, "about:blank", "Test starts with about:blank open"); + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home"); + yield openPreferencesViaOpenPreferencesAPI("paneGeneral", null, {leaveOpen: true}); + let doc = gBrowser.contentDocument; + is(gBrowser.currentURI.spec, "about:preferences#general", + "#general should be in the URI for about:preferences"); + let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage"); + + let useCurrent = doc.getElementById("useCurrent"); + useCurrent.click(); + + is(gBrowser.tabs.length, 3, "Three tabs should be open"); + is(Services.prefs.getCharPref("browser.startup.homepage"), "about:blank|about:home", + "about:blank and about:home should be the only homepages set"); + + Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/in-content/tests/browser_notifications_do_not_disturb.js b/browser/components/preferences/in-content/tests/browser_notifications_do_not_disturb.js new file mode 100644 index 000000000..68f9653f6 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_notifications_do_not_disturb.js @@ -0,0 +1,44 @@ +/* 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(function*() { + let prefs = yield openPreferencesViaOpenPreferencesAPI("paneContent", undefined, {leaveOpen: true}); + is(prefs.selectedPane, "paneContent", "Content pane was selected"); + + let doc = gBrowser.contentDocument; + let notificationsDoNotDisturbRow = doc.getElementById("notificationsDoNotDisturbRow"); + if (notificationsDoNotDisturbRow.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 = waitForEvent(checkbox, "command") + checkbox.click(); + yield checkboxChanged; + ok(alertService.manualDoNotDisturb, "Do not disturb should be enabled when checked"); + + checkboxChanged = waitForEvent(checkbox, "command") + checkbox.click(); + yield checkboxChanged; + ok(!alertService.manualDoNotDisturb, "Do not disturb should be disabled when unchecked"); +}); diff --git a/browser/components/preferences/in-content/tests/browser_permissions_urlFieldHidden.js b/browser/components/preferences/in-content/tests/browser_permissions_urlFieldHidden.js new file mode 100644 index 000000000..d9253735a --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_permissions_urlFieldHidden.js @@ -0,0 +1,45 @@ +"use strict"; + +const PERMISSIONS_URL = "chrome://browser/content/preferences/permissions.xul"; + +add_task(function* urlFieldVisibleForPopupPermissions(finish) { + yield openPreferencesViaOpenPreferencesAPI("paneContent", null, {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 = yield 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(); +}); + +add_task(function* urlFieldHiddenForNotificationPermissions() { + yield openPreferencesViaOpenPreferencesAPI("paneContent", null, {leaveOpen: true}); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + let notificationsPolicyButton = doc.getElementById("notificationsPolicyButton"); + ok(notificationsPolicyButton, "notificationsPolicyButton found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + notificationsPolicyButton.click(); + let dialog = yield dialogPromise; + ok(dialog, "dialog loaded"); + + let urlLabel = dialog.document.getElementById("urlLabel"); + ok(urlLabel.hidden, "urlLabel should be hidden as requested"); + let url = dialog.document.getElementById("url"); + ok(url.hidden, "url should be hidden as requested"); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/in-content/tests/browser_privacypane_1.js b/browser/components/preferences/in-content/tests/browser_privacypane_1.js new file mode 100644 index 000000000..0df60c6ac --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_privacypane_1.js @@ -0,0 +1,18 @@ +let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + '/'; +} +loader.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/in-content/tests/browser_privacypane_3.js b/browser/components/preferences/in-content/tests/browser_privacypane_3.js new file mode 100644 index 000000000..8fe6f0825 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_privacypane_3.js @@ -0,0 +1,17 @@ +let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + '/'; +} +loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this); + +run_test_subset([ + test_custom_retention("rememberHistory", "remember"), + test_custom_retention("rememberHistory", "custom"), + test_custom_retention("rememberForms", "remember"), + test_custom_retention("rememberForms", "custom"), + test_historymode_retention("remember", "remember"), +]); diff --git a/browser/components/preferences/in-content/tests/browser_privacypane_4.js b/browser/components/preferences/in-content/tests/browser_privacypane_4.js new file mode 100644 index 000000000..b7ef3deda --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_privacypane_4.js @@ -0,0 +1,25 @@ +requestLongerTimeout(2); + +let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + '/'; +} +loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this); +let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + +run_test_subset([ + test_custom_retention("acceptCookies", "remember"), + test_custom_retention("acceptCookies", "custom"), + test_custom_retention("acceptThirdPartyMenu", "remember", "visited"), + test_custom_retention("acceptThirdPartyMenu", "custom", "always"), + test_custom_retention("keepCookiesUntil", "remember", 1), + test_custom_retention("keepCookiesUntil", "custom", 2), + test_custom_retention("keepCookiesUntil", "custom", 0), + test_custom_retention("alwaysClear", "remember"), + test_custom_retention("alwaysClear", "custom"), + test_historymode_retention("remember", "remember"), +]); diff --git a/browser/components/preferences/in-content/tests/browser_privacypane_5.js b/browser/components/preferences/in-content/tests/browser_privacypane_5.js new file mode 100644 index 000000000..a07530010 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_privacypane_5.js @@ -0,0 +1,17 @@ +let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + '/'; +} +loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this); + +run_test_subset([ + test_locbar_suggestion_retention("history", true), + test_locbar_suggestion_retention("bookmark", true), + test_locbar_suggestion_retention("openpage", false), + test_locbar_suggestion_retention("history", true), + test_locbar_suggestion_retention("history", false), +]); diff --git a/browser/components/preferences/in-content/tests/browser_privacypane_8.js b/browser/components/preferences/in-content/tests/browser_privacypane_8.js new file mode 100644 index 000000000..756b19a2f --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_privacypane_8.js @@ -0,0 +1,26 @@ +let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + '/'; +} +loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this); + +run_test_subset([ + // history mode should be initialized to remember + test_historymode_retention("remember", undefined), + + // history mode should remain remember; toggle acceptCookies checkbox + test_custom_retention("acceptCookies", "remember"), + + // history mode should now be custom; set history mode to dontremember + test_historymode_retention("dontremember", "custom"), + + // history mode should remain custom; set history mode to remember + test_historymode_retention("remember", "custom"), + + // history mode should now be remember + test_historymode_retention("remember", "remember"), +]); diff --git a/browser/components/preferences/in-content/tests/browser_proxy_backup.js b/browser/components/preferences/in-content/tests/browser_proxy_backup.js new file mode 100644 index 000000000..3ad24c7ec --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_proxy_backup.js @@ -0,0 +1,65 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); + +function test() { + waitForExplicitFinish(); + + // network.proxy.type needs to be backed up and restored because mochitest + // changes this setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function() { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("browser.preferences.instantApply"); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + for (let proxyType of ["http", "ssl", "ftp", "socks"]) { + Services.prefs.clearUserPref("network.proxy." + proxyType); + Services.prefs.clearUserPref("network.proxy." + proxyType + "_port"); + if (proxyType == "http") { + continue; + } + Services.prefs.clearUserPref("network.proxy.backup." + proxyType); + Services.prefs.clearUserPref("network.proxy.backup." + proxyType + "_port"); + } + }); + + let connectionURL = "chrome://browser/content/preferences/connection.xul"; + + // Set a shared proxy and a SOCKS 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.socks", "example.com"); + Services.prefs.setIntPref("network.proxy.socks_port", 1200); + Services.prefs.setCharPref("network.proxy.backup.socks", "127.0.0.1"); + Services.prefs.setIntPref("network.proxy.backup.socks_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(Task.async(function* tabOpened(aContentWindow) { + is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded"); + let dialog = yield openAndLoadSubDialog(connectionURL); + let dialogClosingPromise = waitForEvent(dialog.document.documentElement, "dialogclosing"); + + ok(dialog, "connection window opened"); + dialog.document.documentElement.acceptDialog(); + + let dialogClosingEvent = yield dialogClosingPromise; + ok(dialogClosingEvent, "connection window closed"); + + // The SOCKS backup should not be replaced by the shared value + is(Services.prefs.getCharPref("network.proxy.backup.socks"), "127.0.0.1", "Shared proxy backup shouldn't be replaced"); + is(Services.prefs.getIntPref("network.proxy.backup.socks_port"), 9050, "Shared proxy port backup shouldn't be replaced"); + + gBrowser.removeCurrentTab(); + finish(); + })); +} diff --git a/browser/components/preferences/in-content/tests/browser_sanitizeOnShutdown_prefLocked.js b/browser/components/preferences/in-content/tests/browser_sanitizeOnShutdown_prefLocked.js new file mode 100644 index 000000000..6b587e036 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_sanitizeOnShutdown_prefLocked.js @@ -0,0 +1,37 @@ +"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 = doc.getElementById("privacy.sanitize.sanitizeOnShutdown"); + is(checkbox.disabled, preference.locked, "Always Clear checkbox should be enabled when preference is not locked."); + + gBrowser.removeCurrentTab(); +} + +add_task(function setup() { + registerCleanupFunction(function resetPreferences() { + Services.prefs.unlockPref("privacy.sanitize.sanitizeOnShutdown"); + }); +}); + +add_task(function* test_preference_enabled_when_unlocked() { + yield openPreferencesViaOpenPreferencesAPI("panePrivacy", undefined, {leaveOpen: true}); + testPrefStateMatchesLockedState(); +}); + +add_task(function* test_preference_disabled_when_locked() { + Services.prefs.lockPref("privacy.sanitize.sanitizeOnShutdown"); + yield openPreferencesViaOpenPreferencesAPI("panePrivacy", undefined, {leaveOpen: true}); + testPrefStateMatchesLockedState(); +}); diff --git a/browser/components/preferences/in-content/tests/browser_searchsuggestions.js b/browser/components/preferences/in-content/tests/browser_searchsuggestions.js new file mode 100644 index 000000000..0185a23b9 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_searchsuggestions.js @@ -0,0 +1,43 @@ +var original = Services.prefs.getBoolPref("browser.search.suggest.enabled"); + +registerCleanupFunction(() => { + Services.prefs.setBoolPref("browser.search.suggest.enabled", original); +}); + +// Open with suggestions enabled +add_task(function*() { + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + + yield openPreferencesViaOpenPreferencesAPI("search", undefined, { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let urlbarBox = doc.getElementById("urlBarSuggestion"); + ok(!urlbarBox.disabled, "Checkbox should be enabled"); + + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + ok(urlbarBox.disabled, "Checkbox should be disabled"); + + gBrowser.removeCurrentTab(); +}); + +// Open with suggestions disabled +add_task(function*() { + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + yield openPreferencesViaOpenPreferencesAPI("search", undefined, { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let urlbarBox = doc.getElementById("urlBarSuggestion"); + ok(urlbarBox.disabled, "Checkbox should be disabled"); + + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + + ok(!urlbarBox.disabled, "Checkbox should be enabled"); + + gBrowser.removeCurrentTab(); +}); + +add_task(function*() { + Services.prefs.setBoolPref("browser.search.suggest.enabled", original); +}); diff --git a/browser/components/preferences/in-content/tests/browser_security.js b/browser/components/preferences/in-content/tests/browser_security.js new file mode 100644 index 000000000..e6eb2a91d --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_security.js @@ -0,0 +1,130 @@ +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); +}); + +// test the safebrowsing preference +add_task(function*() { + function* checkPrefSwitch(val1, val2) { + Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled", val1); + Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", val2); + + yield openPreferencesViaOpenPreferencesAPI("security", undefined, { leaveOpen: true }); + + 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(checked, val1 && val2, "safebrowsing preference is initialized correctly"); + // should be disabled when checked is false (= pref is turned off) + is(blockDownloads.hasAttribute("disabled"), !checked, "block downloads checkbox is set correctly"); + is(blockUncommon.hasAttribute("disabled"), !checked, "block uncommon checkbox is set correctly"); + + // click the checkbox + 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; + is(blockDownloads.hasAttribute("disabled"), !checked, "block downloads checkbox is set correctly"); + is(blockUncommon.hasAttribute("disabled"), !checked || !blockDownloads.checked, "block uncommon checkbox is set correctly"); + + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + yield checkPrefSwitch(true, true); + yield checkPrefSwitch(false, true); + yield checkPrefSwitch(true, false); + yield checkPrefSwitch(false, false); +}); + +// test the download protection preference +add_task(function*() { + function* checkPrefSwitch(val) { + Services.prefs.setBoolPref("browser.safebrowsing.downloads.enabled", val); + + yield openPreferencesViaOpenPreferencesAPI("security", undefined, { leaveOpen: true }); + + 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"); + + // click the checkbox + 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"); + + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + yield checkPrefSwitch(true); + yield checkPrefSwitch(false); +}); + +// test the unwanted/uncommon software warning preference +add_task(function*() { + function* checkPrefSwitch(val1, val2) { + Services.prefs.setBoolPref("browser.safebrowsing.downloads.remote.block_potentially_unwanted", val1); + Services.prefs.setBoolPref("browser.safebrowsing.downloads.remote.block_uncommon", val2); + + yield openPreferencesViaOpenPreferencesAPI("security", undefined, { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById("blockUncommonUnwanted"); + let checked = checkbox.checked; + is(checked, val1 && val2, "unwanted/uncommon preference is initialized correctly"); + + // click the checkbox + 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(","); + is(malwareTable.includes("goog-unwanted-shavar"), !checked, + "malware table doesn't include goog-unwanted-shavar"); + is(malwareTable.includes("test-unwanted-simple"), !checked, + "malware table doesn't include test-unwanted-simple"); + let sortedMalware = malwareTable.slice(0); + sortedMalware.sort(); + Assert.deepEqual(malwareTable, sortedMalware, "malware table has been sorted"); + + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + yield* checkPrefSwitch(true, true); + yield* checkPrefSwitch(false, true); + yield* checkPrefSwitch(true, false); + yield* checkPrefSwitch(false, false); +}); diff --git a/browser/components/preferences/in-content/tests/browser_subdialogs.js b/browser/components/preferences/in-content/tests/browser_subdialogs.js new file mode 100644 index 000000000..ff0c1f8ae --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_subdialogs.js @@ -0,0 +1,293 @@ +/* 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.xul"; +const gDialogURL2 = getRootDirectory(gTestPath) + "subdialog2.xul"; + +function* open_subdialog_and_test_generic_start_state(browser, domcontentloadedFn, url = gDialogURL) { + let domcontentloadedFnStr = domcontentloadedFn ? + "(" + domcontentloadedFn.toString() + ")()" : + ""; + return ContentTask.spawn(browser, {url, domcontentloadedFnStr}, function*(args) { + let {url, domcontentloadedFnStr} = args; + let rv = { acceptCount: 0 }; + let win = content.window; + let subdialog = win.gSubDialog; + subdialog.open(url, null, rv); + + info("waiting for subdialog DOMFrameContentLoaded"); + yield ContentTaskUtils.waitForEvent(win, "DOMFrameContentLoaded", true); + let result; + if (domcontentloadedFnStr) { + result = eval(domcontentloadedFnStr); + } + + info("waiting for subdialog load"); + yield ContentTaskUtils.waitForEvent(subdialog._frame, "load"); + 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; + }); +} + +function* close_subdialog_and_test_generic_end_state(browser, closingFn, closingButton, acceptCount, options) { + let dialogclosingPromise = ContentTask.spawn(browser, {closingButton, acceptCount}, function*(expectations) { + let win = content.window; + let subdialog = win.gSubDialog; + let frame = subdialog._frame; + info("waiting for dialogclosing"); + let closingEvent = + yield ContentTaskUtils.waitForEvent(frame.contentWindow, "dialogclosing"); + let closingButton = closingEvent.detail.button; + let actualAcceptCount = frame.contentWindow.arguments && + frame.contentWindow.arguments[0].acceptCount; + + info("waiting for about:blank load"); + yield ContentTaskUtils.waitForEvent(frame, "load"); + + Assert.notEqual(win.getComputedStyle(subdialog._overlay, "").visibility, "visible", + "overlay is not visible"); + Assert.equal(frame.getAttribute("style"), "", "inline styles should be cleared"); + Assert.equal(frame.contentWindow.location.href.toString(), "about:blank", + "sub-dialog should be unloaded"); + Assert.equal(closingButton, 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"); + }); + + if (options && options.runClosingFnOutsideOfContentTask) { + yield closingFn(); + } else { + ContentTask.spawn(browser, null, closingFn); + } + + yield dialogclosingPromise; +} + +let tab; + +add_task(function* test_initialize() { + tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); +}); + +add_task(function* check_titlebar_focus_returnval_titlechanges_accepting() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + let domtitlechangedPromise = BrowserTestUtils.waitForEvent(tab.linkedBrowser, "DOMTitleChanged"); + yield ContentTask.spawn(tab.linkedBrowser, null, function*() { + let dialog = content.window.gSubDialog._frame.contentWindow; + let dialogTitleElement = content.document.getElementById("dialogTitle"); + Assert.equal(dialogTitleElement.textContent, "Sample sub-dialog", + "Title should be correct initially"); + Assert.equal(dialog.document.activeElement.value, "Default text", + "Textbox with correct text is focused"); + dialog.document.title = "Updated title"; + }); + + info("waiting for DOMTitleChanged event"); + yield domtitlechangedPromise; + + ContentTask.spawn(tab.linkedBrowser, null, function*() { + let dialogTitleElement = content.document.getElementById("dialogTitle"); + Assert.equal(dialogTitleElement.textContent, "Updated title", + "subdialog should have updated title"); + }); + + // Accept the dialog + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentDocument.documentElement.acceptDialog(); }, + "accept", 1); +}); + +add_task(function* check_canceling_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentDocument.documentElement.cancelDialog(); }, + "cancel", 0); +}); + +add_task(function* check_reopening_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + info("opening another dialog which will close the first"); + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser, "", gDialogURL2); + info("closing as normal"); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentDocument.documentElement.acceptDialog(); }, + "accept", 1); +}); + +add_task(function* check_opening_while_closing() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + info("closing"); + content.window.gSubDialog.close(); + info("reopening immediately after calling .close()"); + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentDocument.documentElement.acceptDialog(); }, + "accept", 1); + +}); + +add_task(function* window_close_on_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentWindow.window.close(); }, + null, 0); +}); + +add_task(function* click_close_button_on_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { return BrowserTestUtils.synthesizeMouseAtCenter("#dialogClose", {}, tab.linkedBrowser); }, + null, 0, {runClosingFnOutsideOfContentTask: true}); +}); + +add_task(function* back_navigation_on_subdialog_should_close_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.goBack(); }, + null, undefined); +}); + +add_task(function* back_navigation_on_browser_tab_should_close_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { tab.linkedBrowser.goBack(); }, + null, undefined, {runClosingFnOutsideOfContentTask: true}); +}); + +add_task(function* escape_should_close_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { return BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, tab.linkedBrowser); }, + "cancel", 0, {runClosingFnOutsideOfContentTask: true}); +}); + +add_task(function* correct_width_and_height_should_be_used_for_dialog() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + yield ContentTask.spawn(tab.linkedBrowser, null, function*() { + let frameStyle = content.window.gSubDialog._frame.style; + Assert.equal(frameStyle.width, "32em", + "Width should be set on the frame from the dialog"); + Assert.equal(frameStyle.height, "5em", + "Height should be set on the frame from the dialog"); + }); + + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentWindow.window.close(); }, + null, 0); +}); + +add_task(function* wrapped_text_in_dialog_should_have_expected_scrollHeight() { + let oldHeight = yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser, function domcontentloadedFn() { + let frame = content.window.gSubDialog._frame; + let doc = frame.contentDocument; + let oldHeight = 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 oldHeight; + }); + + yield ContentTask.spawn(tab.linkedBrowser, oldHeight, function*(oldHeight) { + let frame = content.window.gSubDialog._frame; + let docEl = frame.contentDocument.documentElement; + Assert.equal(frame.style.width, "32em", + "Width should be set on the frame from the dialog"); + Assert.ok(docEl.scrollHeight > oldHeight, + "Content height increased (from " + oldHeight + " to " + docEl.scrollHeight + ")."); + Assert.equal(frame.style.height, docEl.scrollHeight + "px", + "Height on the frame should be higher now"); + }); + + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentWindow.window.close(); }, + null, 0); +}); + +add_task(function* dialog_too_tall_should_get_reduced_in_height() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser, function domcontentloadedFn() { + let frame = content.window.gSubDialog._frame; + frame.contentDocument.documentElement.style.height = '100000px'; + }); + + yield ContentTask.spawn(tab.linkedBrowser, null, function*() { + let frame = content.window.gSubDialog._frame; + Assert.equal(frame.style.width, "32em", "Width should be set on the frame from the dialog"); + Assert.ok(parseInt(frame.style.height, 10) < content.window.innerHeight, + "Height on the frame should be smaller than window's innerHeight"); + }); + + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentWindow.window.close(); }, + null, 0); +}); + +add_task(function* scrollWidth_and_scrollHeight_from_subdialog_should_size_the_browser() { + yield open_subdialog_and_test_generic_start_state(tab.linkedBrowser, function domcontentloadedFn() { + let frame = content.window.gSubDialog._frame; + frame.contentDocument.documentElement.style.removeProperty("height"); + frame.contentDocument.documentElement.style.removeProperty("width"); + }); + + yield ContentTask.spawn(tab.linkedBrowser, null, function*() { + let frame = content.window.gSubDialog._frame; + Assert.ok(frame.style.width.endsWith("px"), + "Width (" + frame.style.width + ") should be set to a px value of the scrollWidth from the dialog"); + Assert.ok(frame.style.height.endsWith("px"), + "Height (" + frame.style.height + ") should be set to a px value of the scrollHeight from the dialog"); + }); + + yield close_subdialog_and_test_generic_end_state(tab.linkedBrowser, + function() { content.window.gSubDialog._frame.contentWindow.window.close(); }, + null, 0); +}); + +add_task(function* test_shutdown() { + gBrowser.removeTab(tab); +}); diff --git a/browser/components/preferences/in-content/tests/browser_telemetry.js b/browser/components/preferences/in-content/tests/browser_telemetry.js new file mode 100644 index 000000000..d8139d87a --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_telemetry.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. +* http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; + +function runPaneTest(fn) { + open_preferences((win) => { + let doc = win.document; + win.gotoPref("paneAdvanced"); + let advancedPrefs = doc.getElementById("advancedPrefs"); + let tab = doc.getElementById("dataChoicesTab"); + advancedPrefs.selectedTab = tab; + fn(win, doc); + }); +} + +function test() { + waitForExplicitFinish(); + resetPreferences(); + registerCleanupFunction(resetPreferences); + runPaneTest(testTelemetryState); +} + +function testTelemetryState(win, doc) { + let fhrCheckbox = doc.getElementById("submitHealthReportBox"); + Assert.ok(fhrCheckbox.checked, "Health Report checkbox is checked on app first run."); + + let telmetryCheckbox = doc.getElementById("submitTelemetryBox"); + Assert.ok(!telmetryCheckbox.disabled, + "Telemetry checkbox must be enabled if FHR is checked."); + Assert.ok(Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED), + "Telemetry must be enabled if the checkbox is ticked."); + + // Uncheck the FHR checkbox and make sure that Telemetry checkbox gets disabled. + fhrCheckbox.click(); + + Assert.ok(telmetryCheckbox.disabled, + "Telemetry checkbox must be disabled if FHR is unchecked."); + Assert.ok(!Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED), + "Telemetry must be disabled if the checkbox is unticked."); + + win.close(); + finish(); +} + +function resetPreferences() { + Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled"); + Services.prefs.clearUserPref(PREF_TELEMETRY_ENABLED); +} + diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js new file mode 100644 index 000000000..0ed811e94 --- /dev/null +++ b/browser/components/preferences/in-content/tests/head.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/Promise.jsm"); + +const kDefaultWait = 2000; + +function is_hidden(aElement) { + var style = aElement.ownerGlobal.getComputedStyle(aElement); + if (style.display == "none") + return true; + if (style.visibility != "visible") + return true; + + // Hiding a parent element will hide all its children + if (aElement.parentNode != aElement.ownerDocument) + return is_hidden(aElement.parentNode); + + return false; +} + +function is_element_visible(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(!is_hidden(aElement), aMsg); +} + +function is_element_hidden(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(is_hidden(aElement), aMsg); +} + +function open_preferences(aCallback) { + gBrowser.selectedTab = gBrowser.addTab("about:preferences"); + let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + newTabBrowser.addEventListener("Initialized", function () { + newTabBrowser.removeEventListener("Initialized", arguments.callee, true); + aCallback(gBrowser.contentWindow); + }, true); +} + +function openAndLoadSubDialog(aURL, aFeatures = null, aParams = null, aClosingCallback = null) { + let promise = promiseLoadSubDialog(aURL); + content.gSubDialog.open(aURL, aFeatures, aParams, aClosingCallback); + return promise; +} + +function promiseLoadSubDialog(aURL) { + return new Promise((resolve, reject) => { + content.gSubDialog._frame.addEventListener("load", function load(aEvent) { + if (aEvent.target.contentWindow.location == "about:blank") + return; + content.gSubDialog._frame.removeEventListener("load", load); + + is(content.gSubDialog._frame.contentWindow.location.toString(), aURL, + "Check the proper URL is loaded"); + + // Check visibility + is_element_visible(content.gSubDialog._overlay, "Overlay is visible"); + + // Check that stylesheets were injected + let expectedStyleSheetURLs = content.gSubDialog._injectedStyleSheets.slice(0); + for (let styleSheet of content.gSubDialog._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"); + + resolve(content.gSubDialog._frame.contentWindow); + }); + }); +} + +/** + * Waits a specified number of miliseconds for a specified event to be + * fired on a specified element. + * + * Usage: + * let receivedEvent = waitForEvent(element, "eventName"); + * // Do some processing here that will cause the event to be fired + * // ... + * // Now yield until the Promise is fulfilled + * yield receivedEvent; + * if (receivedEvent && !(receivedEvent instanceof Error)) { + * receivedEvent.msg == "eventName"; + * // ... + * } + * + * @param aSubject the element that should receive the event + * @param aEventName the event to wait for + * @param aTimeoutMs the number of miliseconds to wait before giving up + * @returns a Promise that resolves to the received event, or to an Error + */ +function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) { + let eventDeferred = Promise.defer(); + let timeoutMs = aTimeoutMs || kDefaultWait; + let stack = new Error().stack; + let timerID = setTimeout(function wfe_canceller() { + aSubject.removeEventListener(aEventName, listener); + eventDeferred.reject(new Error(aEventName + " event timeout at " + stack)); + }, timeoutMs); + + var listener = function (aEvent) { + if (aTarget && aTarget !== aEvent.target) + return; + + // stop the timeout clock and resume + clearTimeout(timerID); + eventDeferred.resolve(aEvent); + }; + + function cleanup(aEventOrError) { + // unhook listener in case of success or failure + aSubject.removeEventListener(aEventName, listener); + return aEventOrError; + } + aSubject.addEventListener(aEventName, listener, false); + return eventDeferred.promise.then(cleanup, cleanup); +} + +function openPreferencesViaOpenPreferencesAPI(aPane, aAdvancedTab, aOptions) { + let deferred = Promise.defer(); + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + openPreferences(aPane, aAdvancedTab ? {advancedTab: aAdvancedTab} : undefined); + let newTabBrowser = gBrowser.selectedBrowser; + + newTabBrowser.addEventListener("Initialized", function PrefInit() { + newTabBrowser.removeEventListener("Initialized", PrefInit, true); + newTabBrowser.contentWindow.addEventListener("load", function prefLoad() { + newTabBrowser.contentWindow.removeEventListener("load", prefLoad); + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + let doc = win.document; + let selectedAdvancedTab = aAdvancedTab && doc.getElementById("advancedPrefs").selectedTab.id; + if (!aOptions || !aOptions.leaveOpen) + gBrowser.removeCurrentTab(); + deferred.resolve({selectedPane: selectedPane, selectedAdvancedTab: selectedAdvancedTab}); + }); + }, true); + + return deferred.promise; +} + +function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) { + return new Promise((resolve, reject) => { + function tryNow() { + tries++; + let rv = aConditionFn(); + if (rv) { + resolve(rv); + } else if (tries < aMaxTries) { + tryAgain(); + } else { + reject("Condition timed out: " + aConditionFn.toSource()); + } + } + function tryAgain() { + setTimeout(tryNow, aCheckInterval); + } + let tries = 0; + tryAgain(); + }); +} diff --git a/browser/components/preferences/in-content/tests/privacypane_tests_perwindow.js b/browser/components/preferences/in-content/tests/privacypane_tests_perwindow.js new file mode 100644 index 000000000..53c6d7d8a --- /dev/null +++ b/browser/components/preferences/in-content/tests/privacypane_tests_perwindow.js @@ -0,0 +1,330 @@ +function* runTestOnPrivacyPrefPane(testFunc) { + info("runTestOnPrivacyPrefPane entered"); + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences", true, true); + let browser = tab.linkedBrowser; + info("loaded about:preferences"); + browser.contentWindow.gotoPref("panePrivacy"); + info("viewing privacy pane, executing testFunc"); + testFunc(browser.contentWindow); + yield 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("keepUntil"), + win.document.getElementById("keepCookiesUntil"), + win.document.getElementById("alwaysClear"), + ]; + controls.forEach(function(control) { + ok(control, "the dependent controls should exist"); + }); + let independents = [ + win.document.getElementById("acceptCookies"), + win.document.getElementById("acceptThirdPartyLabel"), + win.document.getElementById("acceptThirdPartyMenu") + ]; + 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 keepuntil = win.document.getElementById("keepCookiesUntil"); + ok(keepuntil, "the keep cookies until menulist 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"); + }); + is(keepuntil.value, disabled ? 2 : 0, + "the keep cookies until menulist value should be as expected"); + 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 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("acceptThirdPartyLabel"), + win.document.getElementById("acceptThirdPartyMenu"), + win.document.getElementById("keepUntil"), + win.document.getElementById("keepCookiesUntil"), + ]; + controls.forEach(function(control) { + ok(control, "the dependent cookie controls should exist"); + }); + let acceptcookies = win.document.getElementById("acceptCookies"); + ok(acceptcookies, "the accept cookies checkbox should exist"); + + function expect_disabled(disabled) { + controls.forEach(function(control) { + is(control.disabled, disabled, + control.getAttribute("id") + " should " + (disabled ? "" : "not ") + "be disabled"); + }); + } + + historymode.value = "custom"; + controlChanged(historymode); + pbautostart.checked = false; + controlChanged(pbautostart); + expect_disabled(false); + + acceptcookies.checked = false; + controlChanged(acceptcookies); + expect_disabled(true); + + acceptcookies.checked = true; + controlChanged(acceptcookies); + expect_disabled(false); + + let accessthirdparty = controls.shift(); + acceptcookies.checked = false; + controlChanged(acceptcookies); + expect_disabled(true); + ok(accessthirdparty.disabled, "access third party button should be disabled"); + + pbautostart.checked = false; + controlChanged(pbautostart); + expect_disabled(true); + ok(accessthirdparty.disabled, "access third party button should be disabled"); + + acceptcookies.checked = true; + controlChanged(acceptcookies); + expect_disabled(false); + ok(!accessthirdparty.disabled, "access third party button should be enabled"); +} + +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); +} + +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"), + win.document.getElementById("acceptCookies") + ]; + controls.forEach(function(control) { + ok(control, "the micro-management controls should exist"); + }); + + let thirdPartyCookieMenu = win.document.getElementById("acceptThirdPartyMenu"); + ok(thirdPartyCookieMenu, "the third-party cookie control should exist"); + + function expect_checked(checked) { + controls.forEach(function(control) { + is(control.checked, checked, + control.getAttribute("id") + " should " + (checked ? "not " : "") + "be checked"); + }); + + is(thirdPartyCookieMenu.value == "always" || thirdPartyCookieMenu.value == "visited", checked, "third-party cookies should " + (checked ? "not " : "") + "be limited"); + } + + // controls should be checked in remember mode + historymode.value = "remember"; + controlChanged(historymode); + expect_checked(true); + + // even if they're unchecked in custom mode + historymode.value = "custom"; + controlChanged(historymode); + thirdPartyCookieMenu.value = "never"; + controlChanged(thirdPartyCookieMenu); + 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 "textbox": + controlToChange.value = parseInt(controlToChange.value) + valueIncrement; + break; + case "menulist": + controlToChange.value = valueIncrement; + break; + } + controlChanged(controlToChange); + }; +} + +function test_locbar_suggestion_retention(suggestion, autocomplete) { + return function(win) { + let elem = win.document.getElementById(suggestion + "Suggestion"); + ok(elem, "Suggest " + suggestion + " checkbox should exist."); + elem.click(); + + is(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), autocomplete, + "browser.urlbar.autocomplete.enabled pref should be " + autocomplete); + }; +} + +const gPrefCache = new Map(); + +function cache_preferences(win) { + let prefs = win.document.querySelectorAll("#privacyPreferences > preference"); + for (let pref of prefs) + gPrefCache.set(pref.name, pref.value); +} + +function reset_preferences(win) { + let prefs = win.document.querySelectorAll("#privacyPreferences > preference"); + for (let pref of prefs) + pref.value = gPrefCache.get(pref.name); +} + +function run_test_subset(subset) { + info("subset: " + Array.from(subset, x => x.name).join(",") + "\n"); + SpecialPowers.pushPrefEnv({"set": [["browser.preferences.instantApply", true]]}); + + let tests = [cache_preferences, ...subset, reset_preferences]; + for (let test of tests) { + add_task(runTestOnPrivacyPrefPane.bind(undefined, test)); + } +} diff --git a/browser/components/preferences/in-content/tests/subdialog.xul b/browser/components/preferences/in-content/tests/subdialog.xul new file mode 100644 index 000000000..48d297b73 --- /dev/null +++ b/browser/components/preferences/in-content/tests/subdialog.xul @@ -0,0 +1,27 @@ +<?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/" type="text/css"?> + +<dialog id="subDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Sample sub-dialog" style="width: 32em; height: 5em;" + onload="document.getElementById('textbox').focus();" + ondialogaccept="acceptSubdialog();"> + <script> + function acceptSubdialog() { + window.arguments[0].acceptCount++; + } + </script> + + <description id="desc">A sample sub-dialog for testing</description> + + <textbox id="textbox" value="Default text" /> + + <separator class="thin"/> + + <button oncommand="close();" icon="close" label="Close" /> + +</dialog> diff --git a/browser/components/preferences/in-content/tests/subdialog2.xul b/browser/components/preferences/in-content/tests/subdialog2.xul new file mode 100644 index 000000000..89803c250 --- /dev/null +++ b/browser/components/preferences/in-content/tests/subdialog2.xul @@ -0,0 +1,27 @@ +<?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/" type="text/css"?> + +<dialog id="subDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Sample sub-dialog #2" style="width: 32em; height: 5em;" + onload="document.getElementById('textbox').focus();" + ondialogaccept="acceptSubdialog();"> + <script> + function acceptSubdialog() { + window.arguments[0].acceptCount++; + } + </script> + + <description id="desc">A sample sub-dialog for testing</description> + + <textbox id="textbox" value="Default text" /> + + <separator class="thin"/> + + <button oncommand="close();" icon="close" label="Close" /> + +</dialog> |