summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/components/passwordmgr
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-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 'toolkit/components/passwordmgr')
-rw-r--r--toolkit/components/passwordmgr/.eslintrc.js36
-rw-r--r--toolkit/components/passwordmgr/InsecurePasswordUtils.jsm150
-rw-r--r--toolkit/components/passwordmgr/LoginHelper.jsm725
-rw-r--r--toolkit/components/passwordmgr/LoginImport.jsm173
-rw-r--r--toolkit/components/passwordmgr/LoginManagerContent.jsm1619
-rw-r--r--toolkit/components/passwordmgr/LoginManagerContextMenu.jsm199
-rw-r--r--toolkit/components/passwordmgr/LoginManagerParent.jsm511
-rw-r--r--toolkit/components/passwordmgr/LoginRecipes.jsm260
-rw-r--r--toolkit/components/passwordmgr/LoginStore.jsm136
-rw-r--r--toolkit/components/passwordmgr/OSCrypto.jsm22
-rw-r--r--toolkit/components/passwordmgr/OSCrypto_win.js245
-rw-r--r--toolkit/components/passwordmgr/content/passwordManager.js728
-rw-r--r--toolkit/components/passwordmgr/content/passwordManager.xul134
-rw-r--r--toolkit/components/passwordmgr/content/recipes.json31
-rw-r--r--toolkit/components/passwordmgr/crypto-SDR.js207
-rw-r--r--toolkit/components/passwordmgr/jar.mn9
-rw-r--r--toolkit/components/passwordmgr/moz.build78
-rw-r--r--toolkit/components/passwordmgr/nsILoginInfo.idl120
-rw-r--r--toolkit/components/passwordmgr/nsILoginManager.idl262
-rw-r--r--toolkit/components/passwordmgr/nsILoginManagerCrypto.idl67
-rw-r--r--toolkit/components/passwordmgr/nsILoginManagerPrompter.idl94
-rw-r--r--toolkit/components/passwordmgr/nsILoginManagerStorage.idl211
-rw-r--r--toolkit/components/passwordmgr/nsILoginMetaInfo.idl55
-rw-r--r--toolkit/components/passwordmgr/nsLoginInfo.js93
-rw-r--r--toolkit/components/passwordmgr/nsLoginManager.js541
-rw-r--r--toolkit/components/passwordmgr/nsLoginManagerPrompter.js1701
-rw-r--r--toolkit/components/passwordmgr/passwordmgr.manifest17
-rw-r--r--toolkit/components/passwordmgr/storage-json.js514
-rw-r--r--toolkit/components/passwordmgr/storage-mozStorage.js1262
-rw-r--r--toolkit/components/passwordmgr/test/.eslintrc.js13
-rw-r--r--toolkit/components/passwordmgr/test/LoginTestUtils.jsm295
-rw-r--r--toolkit/components/passwordmgr/test/authenticate.sjs228
-rw-r--r--toolkit/components/passwordmgr/test/blank.html8
-rw-r--r--toolkit/components/passwordmgr/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/passwordmgr/test/browser/authenticate.sjs110
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser.ini72
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js94
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js99
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js41
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js600
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js123
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js144
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu.js432
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js99
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js144
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js56
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js126
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js93
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js102
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_http_autofill.js78
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js94
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js59
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications.js81
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications_2.js125
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications_password.js145
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications_username.js119
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js100
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js126
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js65
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js129
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js208
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js42
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js192
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js144
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_autofocus_js.html10
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic_iframe.html13
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_same_origin_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/formless_basic.html18
-rw-r--r--toolkit/components/passwordmgr/test/browser/head.js137
-rw-r--r--toolkit/components/passwordmgr/test/browser/insecure_test.html9
-rw-r--r--toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html13
-rw-r--r--toolkit/components/passwordmgr/test/browser/multiple_forms.html129
-rw-r--r--toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs6
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html25
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html32
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html31
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html26
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html32
-rw-r--r--toolkit/components/passwordmgr/test/chrome/chrome.ini13
-rw-r--r--toolkit/components/passwordmgr/test/chrome/notification_common.js111
-rw-r--r--toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html9
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html33
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html33
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html29
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html40
-rw-r--r--toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html322
-rw-r--r--toolkit/components/passwordmgr/test/chrome_timeout.js11
-rw-r--r--toolkit/components/passwordmgr/test/formsubmit.sjs37
-rw-r--r--toolkit/components/passwordmgr/test/mochitest.ini20
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs220
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/mochitest.ini69
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html218
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html117
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html143
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html115
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form.html44
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html72
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html167
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html109
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html187
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html105
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html177
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html859
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html164
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html55
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html213
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html145
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html56
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_case_differences.html147
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html137
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html170
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html52
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html147
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html183
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html191
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html121
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_input_events.html96
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html51
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html861
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html103
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_maxlength.html137
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html291
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html122
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt.html705
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html362
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html81
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html406
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html264
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html145
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_username_focus.html263
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html55
-rw-r--r--toolkit/components/passwordmgr/test/prompt_common.js79
-rw-r--r--toolkit/components/passwordmgr/test/pwmgr_common.js509
-rw-r--r--toolkit/components/passwordmgr/test/subtst_master_pass.html12
-rw-r--r--toolkit/components/passwordmgr/test/subtst_prompt_async.html12
-rw-r--r--toolkit/components/passwordmgr/test/test_master_password.html308
-rw-r--r--toolkit/components/passwordmgr/test/test_prompt_async.html540
-rw-r--r--toolkit/components/passwordmgr/test/test_xhr.html201
-rw-r--r--toolkit/components/passwordmgr/test/test_xml_load.html191
-rw-r--r--toolkit/components/passwordmgr/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/key3.dbbin0 -> 16384 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlitebin0 -> 8192 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlitebin0 -> 10240 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlitebin0 -> 12288 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlitebin0 -> 294912 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlitebin0 -> 327680 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlitebin0 -> 327680 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlitebin0 -> 8192 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/head.js135
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js75
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_context_menu.js165
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js284
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js196
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_getFormFields.js147
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js156
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js28
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js40
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js107
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_legacy_validation.js76
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_change.js384
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js77
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js284
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_search.js221
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js169
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js243
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js206
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_notifications.js172
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_recipes_add.js177
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_recipes_content.js39
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js69
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js184
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_storage.js102
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js507
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_telemetry.js187
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js488
-rw-r--r--toolkit/components/passwordmgr/test/unit/xpcshell.ini46
193 files changed, 31386 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/.eslintrc.js b/toolkit/components/passwordmgr/.eslintrc.js
new file mode 100644
index 000000000..188f7eeff
--- /dev/null
+++ b/toolkit/components/passwordmgr/.eslintrc.js
@@ -0,0 +1,36 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../.eslintrc.js",
+ "rules": {
+ // Require spacing around =>
+ "arrow-spacing": "error",
+
+ // No newline before open brace for a block
+ "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+
+ // No space before always a space after a comma
+ "comma-spacing": ["error", {"before": false, "after": true}],
+
+ // Commas at the end of the line not the start
+ "comma-style": "error",
+
+ // Use [] instead of Array()
+ "no-array-constructor": "error",
+
+ // Use {} instead of new Object()
+ "no-new-object": "error",
+
+ // No using undeclared variables
+ "no-undef": "error",
+
+ // Don't allow unused local variables unless they match the pattern
+ "no-unused-vars": ["error", {"args": "none", "vars": "local", "varsIgnorePattern": "^(ids|ignored|unused)$"}],
+
+ // Always require semicolon at end of statement
+ "semi": ["error", "always"],
+
+ // Require spaces around operators
+ "space-infix-ops": "error",
+ }
+};
diff --git a/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm b/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
new file mode 100644
index 000000000..5351e45b2
--- /dev/null
+++ b/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = [ "InsecurePasswordUtils" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const STRINGS_URI = "chrome://global/locale/security/security.properties";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
+ "@mozilla.org/contentsecuritymanager;1",
+ "nsIContentSecurityManager");
+XPCOMUtils.defineLazyServiceGetter(this, "gScriptSecurityManager",
+ "@mozilla.org/scriptsecuritymanager;1",
+ "nsIScriptSecurityManager");
+XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", () => {
+ return this.devtools.require("devtools/server/actors/utils/webconsole-utils").Utils;
+});
+
+/*
+ * A module that provides utility functions for form security.
+ *
+ * Note:
+ * This module uses isSecureContextIfOpenerIgnored instead of isSecureContext.
+ *
+ * We don't want to expose JavaScript APIs in a non-Secure Context even if
+ * the context is only insecure because the windows has an insecure opener.
+ * Doing so prevents sites from implementing postMessage workarounds to enable
+ * an insecure opener to gain access to Secure Context-only APIs. However,
+ * in the case of form fields such as password fields we don't need to worry
+ * about whether the opener is secure or not. In fact to flag a password
+ * field as insecure in such circumstances would unnecessarily confuse our
+ * users.
+ */
+this.InsecurePasswordUtils = {
+ _formRootsWarned: new WeakMap(),
+ _sendWebConsoleMessage(messageTag, domDoc) {
+ let windowId = WebConsoleUtils.getInnerWindowId(domDoc.defaultView);
+ let category = "Insecure Password Field";
+ // All web console messages are warnings for now.
+ let flag = Ci.nsIScriptError.warningFlag;
+ let bundle = Services.strings.createBundle(STRINGS_URI);
+ let message = bundle.GetStringFromName(messageTag);
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+ consoleMsg.initWithWindowID(message, domDoc.location.href, 0, 0, 0, flag, category, windowId);
+
+ Services.console.logMessage(consoleMsg);
+ },
+
+ /**
+ * Gets the security state of the passed form.
+ *
+ * @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
+ *
+ * @returns {Object} An object with the following boolean values:
+ * isFormSubmitHTTP: if the submit action is an http:// URL
+ * isFormSubmitSecure: if the submit action URL is secure,
+ * either because it is HTTPS or because its origin is considered trustworthy
+ */
+ _checkFormSecurity(aForm) {
+ let isFormSubmitHTTP = false, isFormSubmitSecure = false;
+ if (aForm.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
+ let uri = Services.io.newURI(aForm.rootElement.action || aForm.rootElement.baseURI,
+ null, null);
+ let principal = gScriptSecurityManager.getCodebasePrincipal(uri);
+
+ if (uri.schemeIs("http")) {
+ isFormSubmitHTTP = true;
+ if (gContentSecurityManager.isOriginPotentiallyTrustworthy(principal)) {
+ isFormSubmitSecure = true;
+ }
+ } else {
+ isFormSubmitSecure = true;
+ }
+ }
+
+ return { isFormSubmitHTTP, isFormSubmitSecure };
+ },
+
+ /**
+ * Checks if there are insecure password fields present on the form's document
+ * i.e. passwords inside forms with http action, inside iframes with http src,
+ * or on insecure web pages.
+ *
+ * @param {FormLike} aForm A form-like object. @See {LoginFormFactory}
+ * @return {boolean} whether the form is secure
+ */
+ isFormSecure(aForm) {
+ // Ignores window.opener, see top level documentation.
+ let isSafePage = aForm.ownerDocument.defaultView.isSecureContextIfOpenerIgnored;
+ let { isFormSubmitSecure, isFormSubmitHTTP } = this._checkFormSecurity(aForm);
+
+ return isSafePage && (isFormSubmitSecure || !isFormSubmitHTTP);
+ },
+
+ /**
+ * Report insecure password fields in a form to the web console to warn developers.
+ *
+ * @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
+ */
+ reportInsecurePasswords(aForm) {
+ if (this._formRootsWarned.has(aForm.rootElement) ||
+ this._formRootsWarned.get(aForm.rootElement)) {
+ return;
+ }
+
+ let domDoc = aForm.ownerDocument;
+ // Ignores window.opener, see top level documentation.
+ let isSafePage = domDoc.defaultView.isSecureContextIfOpenerIgnored;
+
+ let { isFormSubmitHTTP, isFormSubmitSecure } = this._checkFormSecurity(aForm);
+
+ if (!isSafePage) {
+ if (domDoc.defaultView == domDoc.defaultView.parent) {
+ this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc);
+ } else {
+ this._sendWebConsoleMessage("InsecurePasswordsPresentOnIframe", domDoc);
+ }
+ this._formRootsWarned.set(aForm.rootElement, true);
+ } else if (isFormSubmitHTTP && !isFormSubmitSecure) {
+ this._sendWebConsoleMessage("InsecureFormActionPasswordsPresent", domDoc);
+ this._formRootsWarned.set(aForm.rootElement, true);
+ }
+
+ // The safety of a password field determined by the form action and the page protocol
+ let passwordSafety;
+ if (isSafePage) {
+ if (isFormSubmitSecure) {
+ passwordSafety = 0;
+ } else if (isFormSubmitHTTP) {
+ passwordSafety = 1;
+ } else {
+ passwordSafety = 2;
+ }
+ } else if (isFormSubmitSecure) {
+ passwordSafety = 3;
+ } else if (isFormSubmitHTTP) {
+ passwordSafety = 4;
+ } else {
+ passwordSafety = 5;
+ }
+
+ Services.telemetry.getHistogramById("PWMGR_LOGIN_PAGE_SAFETY").add(passwordSafety);
+ },
+};
diff --git a/toolkit/components/passwordmgr/LoginHelper.jsm b/toolkit/components/passwordmgr/LoginHelper.jsm
new file mode 100644
index 000000000..e0c4d872b
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -0,0 +1,725 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Contains functions shared by different Login Manager components.
+ *
+ * This JavaScript module exists in order to share code between the different
+ * XPCOM components that constitute the Login Manager, including implementations
+ * of nsILoginManager and nsILoginManagerStorage.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginHelper",
+];
+
+// Globals
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// LoginHelper
+
+/**
+ * Contains functions shared by different Login Manager components.
+ */
+this.LoginHelper = {
+ /**
+ * Warning: these only update if a logger was created.
+ */
+ debug: Services.prefs.getBoolPref("signon.debug"),
+ formlessCaptureEnabled: Services.prefs.getBoolPref("signon.formlessCapture.enabled"),
+ schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ insecureAutofill: Services.prefs.getBoolPref("signon.autofillForms.http"),
+ showInsecureFieldWarning: Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled"),
+
+ createLogger(aLogPrefix) {
+ let getMaxLogLevel = () => {
+ return this.debug ? "debug" : "warn";
+ };
+
+ // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+ let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
+ let consoleOptions = {
+ maxLogLevel: getMaxLogLevel(),
+ prefix: aLogPrefix,
+ };
+ let logger = new ConsoleAPI(consoleOptions);
+
+ // Watch for pref changes and update this.debug and the maxLogLevel for created loggers
+ Services.prefs.addObserver("signon.", () => {
+ this.debug = Services.prefs.getBoolPref("signon.debug");
+ this.formlessCaptureEnabled = Services.prefs.getBoolPref("signon.formlessCapture.enabled");
+ this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
+ this.insecureAutofill = Services.prefs.getBoolPref("signon.autofillForms.http");
+ logger.maxLogLevel = getMaxLogLevel();
+ }, false);
+
+ Services.prefs.addObserver("security.insecure_field_warning.", () => {
+ this.showInsecureFieldWarning = Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled");
+ }, false);
+
+ return logger;
+ },
+
+ /**
+ * Due to the way the signons2.txt file is formatted, we need to make
+ * sure certain field values or characters do not cause the file to
+ * be parsed incorrectly. Reject hostnames that we can't store correctly.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ checkHostnameValue(aHostname) {
+ // Nulls are invalid, as they don't round-trip well. Newlines are also
+ // invalid for any field stored as plaintext, and a hostname made of a
+ // single dot cannot be stored in the legacy format.
+ if (aHostname == "." ||
+ aHostname.indexOf("\r") != -1 ||
+ aHostname.indexOf("\n") != -1 ||
+ aHostname.indexOf("\0") != -1) {
+ throw new Error("Invalid hostname");
+ }
+ },
+
+ /**
+ * Due to the way the signons2.txt file is formatted, we need to make
+ * sure certain field values or characters do not cause the file to
+ * be parsed incorrectly. Reject logins that we can't store correctly.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ checkLoginValues(aLogin) {
+ function badCharacterPresent(l, c) {
+ return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
+ (l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
+ l.hostname.indexOf(c) != -1 ||
+ l.usernameField.indexOf(c) != -1 ||
+ l.passwordField.indexOf(c) != -1);
+ }
+
+ // Nulls are invalid, as they don't round-trip well.
+ // Mostly not a formatting problem, although ".\0" can be quirky.
+ if (badCharacterPresent(aLogin, "\0")) {
+ throw new Error("login values can't contain nulls");
+ }
+
+ // In theory these nulls should just be rolled up into the encrypted
+ // values, but nsISecretDecoderRing doesn't use nsStrings, so the
+ // nulls cause truncation. Check for them here just to avoid
+ // unexpected round-trip surprises.
+ if (aLogin.username.indexOf("\0") != -1 ||
+ aLogin.password.indexOf("\0") != -1) {
+ throw new Error("login values can't contain nulls");
+ }
+
+ // Newlines are invalid for any field stored as plaintext.
+ if (badCharacterPresent(aLogin, "\r") ||
+ badCharacterPresent(aLogin, "\n")) {
+ throw new Error("login values can't contain newlines");
+ }
+
+ // A line with just a "." can have special meaning.
+ if (aLogin.usernameField == "." ||
+ aLogin.formSubmitURL == ".") {
+ throw new Error("login values can't be periods");
+ }
+
+ // A hostname with "\ \(" won't roundtrip.
+ // eg host="foo (", realm="bar" --> "foo ( (bar)"
+ // vs host="foo", realm=" (bar" --> "foo ( (bar)"
+ if (aLogin.hostname.indexOf(" (") != -1) {
+ throw new Error("bad parens in hostname");
+ }
+ },
+
+ /**
+ * Returns a new XPCOM property bag with the provided properties.
+ *
+ * @param {Object} aProperties
+ * Each property of this object is copied to the property bag. This
+ * parameter can be omitted to return an empty property bag.
+ *
+ * @return A new property bag, that is an instance of nsIWritablePropertyBag,
+ * nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
+ */
+ newPropertyBag(aProperties) {
+ let propertyBag = Cc["@mozilla.org/hash-property-bag;1"]
+ .createInstance(Ci.nsIWritablePropertyBag);
+ if (aProperties) {
+ for (let [name, value] of Object.entries(aProperties)) {
+ propertyBag.setProperty(name, value);
+ }
+ }
+ return propertyBag.QueryInterface(Ci.nsIPropertyBag)
+ .QueryInterface(Ci.nsIPropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag2);
+ },
+
+ /**
+ * Helper to avoid the `count` argument and property bags when calling
+ * Services.logins.searchLogins from JS.
+ *
+ * @param {Object} aSearchOptions - A regular JS object to copy to a property bag before searching
+ * @return {nsILoginInfo[]} - The result of calling searchLogins.
+ */
+ searchLoginsWithObject(aSearchOptions) {
+ return Services.logins.searchLogins({}, this.newPropertyBag(aSearchOptions));
+ },
+
+ /**
+ * @param {String} aLoginOrigin - An origin value from a stored login's
+ * hostname or formSubmitURL properties.
+ * @param {String} aSearchOrigin - The origin that was are looking to match
+ * with aLoginOrigin. This would normally come
+ * from a form or page that we are considering.
+ * @param {nsILoginFindOptions} aOptions - Options to affect whether the origin
+ * from the login (aLoginOrigin) is a
+ * match for the origin we're looking
+ * for (aSearchOrigin).
+ */
+ isOriginMatching(aLoginOrigin, aSearchOrigin, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ if (aLoginOrigin == aSearchOrigin) {
+ return true;
+ }
+
+ if (!aOptions) {
+ return false;
+ }
+
+ if (aOptions.schemeUpgrades) {
+ try {
+ let loginURI = Services.io.newURI(aLoginOrigin, null, null);
+ let searchURI = Services.io.newURI(aSearchOrigin, null, null);
+ if (loginURI.scheme == "http" && searchURI.scheme == "https" &&
+ loginURI.hostPort == searchURI.hostPort) {
+ return true;
+ }
+ } catch (ex) {
+ // newURI will throw for some values e.g. chrome://FirefoxAccounts
+ return false;
+ }
+ }
+
+ return false;
+ },
+
+ doLoginsMatch(aLogin1, aLogin2, {
+ ignorePassword = false,
+ ignoreSchemes = false,
+ }) {
+ if (aLogin1.httpRealm != aLogin2.httpRealm ||
+ aLogin1.username != aLogin2.username)
+ return false;
+
+ if (!ignorePassword && aLogin1.password != aLogin2.password)
+ return false;
+
+ if (ignoreSchemes) {
+ let hostname1URI = Services.io.newURI(aLogin1.hostname, null, null);
+ let hostname2URI = Services.io.newURI(aLogin2.hostname, null, null);
+ if (hostname1URI.hostPort != hostname2URI.hostPort)
+ return false;
+
+ if (aLogin1.formSubmitURL != "" && aLogin2.formSubmitURL != "" &&
+ Services.io.newURI(aLogin1.formSubmitURL, null, null).hostPort !=
+ Services.io.newURI(aLogin2.formSubmitURL, null, null).hostPort)
+ return false;
+ } else {
+ if (aLogin1.hostname != aLogin2.hostname)
+ return false;
+
+ // If either formSubmitURL is blank (but not null), then match.
+ if (aLogin1.formSubmitURL != "" && aLogin2.formSubmitURL != "" &&
+ aLogin1.formSubmitURL != aLogin2.formSubmitURL)
+ return false;
+ }
+
+ // The .usernameField and .passwordField values are ignored.
+
+ return true;
+ },
+
+ /**
+ * Creates a new login object that results by modifying the given object with
+ * the provided data.
+ *
+ * @param aOldStoredLogin
+ * Existing nsILoginInfo object to modify.
+ * @param aNewLoginData
+ * The new login values, either as nsILoginInfo or nsIProperyBag.
+ *
+ * @return The newly created nsILoginInfo object.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ buildModifiedLogin(aOldStoredLogin, aNewLoginData) {
+ function bagHasProperty(aPropName) {
+ try {
+ aNewLoginData.getProperty(aPropName);
+ return true;
+ } catch (ex) { }
+ return false;
+ }
+
+ aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ let newLogin;
+ if (aNewLoginData instanceof Ci.nsILoginInfo) {
+ // Clone the existing login to get its nsILoginMetaInfo, then init it
+ // with the replacement nsILoginInfo data from the new login.
+ newLogin = aOldStoredLogin.clone();
+ newLogin.init(aNewLoginData.hostname,
+ aNewLoginData.formSubmitURL, aNewLoginData.httpRealm,
+ aNewLoginData.username, aNewLoginData.password,
+ aNewLoginData.usernameField, aNewLoginData.passwordField);
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // Automatically update metainfo when password is changed.
+ if (newLogin.password != aOldStoredLogin.password) {
+ newLogin.timePasswordChanged = Date.now();
+ }
+ } else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
+ // Clone the existing login, along with all its properties.
+ newLogin = aOldStoredLogin.clone();
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // Automatically update metainfo when password is changed.
+ // (Done before the main property updates, lest the caller be
+ // explicitly updating both .password and .timePasswordChanged)
+ if (bagHasProperty("password")) {
+ let newPassword = aNewLoginData.getProperty("password");
+ if (newPassword != aOldStoredLogin.password) {
+ newLogin.timePasswordChanged = Date.now();
+ }
+ }
+
+ let propEnum = aNewLoginData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // nsILoginInfo
+ case "hostname":
+ case "httpRealm":
+ case "formSubmitURL":
+ case "username":
+ case "password":
+ case "usernameField":
+ case "passwordField":
+ // nsILoginMetaInfo
+ case "guid":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ newLogin[prop.name] = prop.value;
+ break;
+
+ // Fake property, allows easy incrementing.
+ case "timesUsedIncrement":
+ newLogin.timesUsed += prop.value;
+ break;
+
+ // Fail if caller requests setting an unknown property.
+ default:
+ throw new Error("Unexpected propertybag item: " + prop.name);
+ }
+ }
+ } else {
+ throw new Error("newLoginData needs an expected interface!");
+ }
+
+ // Sanity check the login
+ if (newLogin.hostname == null || newLogin.hostname.length == 0) {
+ throw new Error("Can't add a login with a null or empty hostname.");
+ }
+
+ // For logins w/o a username, set to "", not null.
+ if (newLogin.username == null) {
+ throw new Error("Can't add a login with a null username.");
+ }
+
+ if (newLogin.password == null || newLogin.password.length == 0) {
+ throw new Error("Can't add a login with a null or empty password.");
+ }
+
+ if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
+ // We have a form submit URL. Can't have a HTTP realm.
+ if (newLogin.httpRealm != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else if (newLogin.httpRealm) {
+ // We have a HTTP realm. Can't have a form submit URL.
+ if (newLogin.formSubmitURL != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else {
+ // Need one or the other!
+ throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
+ }
+
+ // Throws if there are bogus values.
+ this.checkLoginValues(newLogin);
+
+ return newLogin;
+ },
+
+ /**
+ * Removes duplicates from a list of logins while preserving the sort order.
+ *
+ * @param {nsILoginInfo[]} logins
+ * A list of logins we want to deduplicate.
+ * @param {string[]} [uniqueKeys = ["username", "password"]]
+ * A list of login attributes to use as unique keys for the deduplication.
+ * @param {string[]} [resolveBy = ["timeLastUsed"]]
+ * Ordered array of keyword strings used to decide which of the
+ * duplicates should be used. "scheme" would prefer the login that has
+ * a scheme matching `preferredOrigin`'s if there are two logins with
+ * the same `uniqueKeys`. The default preference to distinguish two
+ * logins is `timeLastUsed`. If there is no preference between two
+ * logins, the first one found wins.
+ * @param {string} [preferredOrigin = undefined]
+ * String representing the origin to use for preferring one login over
+ * another when they are dupes. This is used with "scheme" for
+ * `resolveBy` so the scheme from this origin will be preferred.
+ *
+ * @returns {nsILoginInfo[]} list of unique logins.
+ */
+ dedupeLogins(logins, uniqueKeys = ["username", "password"],
+ resolveBy = ["timeLastUsed"],
+ preferredOrigin = undefined) {
+ const KEY_DELIMITER = ":";
+
+ if (!preferredOrigin && resolveBy.includes("scheme")) {
+ throw new Error("dedupeLogins: `preferredOrigin` is required in order to " +
+ "prefer schemes which match it.");
+ }
+
+ let preferredOriginScheme;
+ if (preferredOrigin) {
+ try {
+ preferredOriginScheme = Services.io.newURI(preferredOrigin, null, null).scheme;
+ } catch (ex) {
+ // Handle strings that aren't valid URIs e.g. chrome://FirefoxAccounts
+ }
+ }
+
+ if (!preferredOriginScheme && resolveBy.includes("scheme")) {
+ log.warn("dedupeLogins: Deduping with a scheme preference but couldn't " +
+ "get the preferred origin scheme.");
+ }
+
+ // We use a Map to easily lookup logins by their unique keys.
+ let loginsByKeys = new Map();
+
+ // Generate a unique key string from a login.
+ function getKey(login, uniqueKeys) {
+ return uniqueKeys.reduce((prev, key) => prev + KEY_DELIMITER + login[key], "");
+ }
+
+ /**
+ * @return {bool} whether `login` is preferred over its duplicate (considering `uniqueKeys`)
+ * `existingLogin`.
+ *
+ * `resolveBy` is a sorted array so we can return true the first time `login` is preferred
+ * over the existingLogin.
+ */
+ function isLoginPreferred(existingLogin, login) {
+ if (!resolveBy || resolveBy.length == 0) {
+ // If there is no preference, prefer the existing login.
+ return false;
+ }
+
+ for (let preference of resolveBy) {
+ switch (preference) {
+ case "scheme": {
+ if (!preferredOriginScheme) {
+ break;
+ }
+
+ try {
+ // Only `hostname` is currently considered
+ let existingLoginURI = Services.io.newURI(existingLogin.hostname, null, null);
+ let loginURI = Services.io.newURI(login.hostname, null, null);
+ // If the schemes of the two logins are the same or neither match the
+ // preferredOriginScheme then we have no preference and look at the next resolveBy.
+ if (loginURI.scheme == existingLoginURI.scheme ||
+ (loginURI.scheme != preferredOriginScheme &&
+ existingLoginURI.scheme != preferredOriginScheme)) {
+ break;
+ }
+
+ return loginURI.scheme == preferredOriginScheme;
+ } catch (ex) {
+ // Some URLs aren't valid nsIURI (e.g. chrome://FirefoxAccounts)
+ log.debug("dedupeLogins/shouldReplaceExisting: Error comparing schemes:",
+ existingLogin.hostname, login.hostname,
+ "preferredOrigin:", preferredOrigin, ex);
+ }
+ break;
+ }
+ case "timeLastUsed":
+ case "timePasswordChanged": {
+ // If we find a more recent login for the same key, replace the existing one.
+ let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo)[preference];
+ let storedLoginDate = existingLogin.QueryInterface(Ci.nsILoginMetaInfo)[preference];
+ if (loginDate == storedLoginDate) {
+ break;
+ }
+
+ return loginDate > storedLoginDate;
+ }
+ default: {
+ throw new Error("dedupeLogins: Invalid resolveBy preference: " + preference);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ for (let login of logins) {
+ let key = getKey(login, uniqueKeys);
+
+ if (loginsByKeys.has(key)) {
+ if (!isLoginPreferred(loginsByKeys.get(key), login)) {
+ // If there is no preference for the new login, use the existing one.
+ continue;
+ }
+ }
+ loginsByKeys.set(key, login);
+ }
+
+ // Return the map values in the form of an array.
+ return [...loginsByKeys.values()];
+ },
+
+ /**
+ * Open the password manager window.
+ *
+ * @param {Window} window
+ * the window from where we want to open the dialog
+ *
+ * @param {string} [filterString=""]
+ * the filterString parameter to pass to the login manager dialog
+ */
+ openPasswordManager(window, filterString = "") {
+ let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
+ if (win) {
+ win.setFilter(filterString);
+ win.focus();
+ } else {
+ window.openDialog("chrome://passwordmgr/content/passwordManager.xul",
+ "Toolkit:PasswordManager", "",
+ {filterString : filterString});
+ }
+ },
+
+ /**
+ * Checks if a field type is username compatible.
+ *
+ * @param {Element} element
+ * the field we want to check.
+ *
+ * @returns {Boolean} true if the field type is one
+ * of the username types.
+ */
+ isUsernameFieldType(element) {
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement))
+ return false;
+
+ let fieldType = (element.hasAttribute("type") ?
+ element.getAttribute("type").toLowerCase() :
+ element.type);
+ if (fieldType == "text" ||
+ fieldType == "email" ||
+ fieldType == "url" ||
+ fieldType == "tel" ||
+ fieldType == "number") {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Add the login to the password manager if a similar one doesn't already exist. Merge it
+ * otherwise with the similar existing ones.
+ * @param {Object} loginData - the data about the login that needs to be added.
+ * @returns {nsILoginInfo} the newly added login, or null if no login was added.
+ * Note that we will also return null if an existing login
+ * was modified.
+ */
+ maybeImportLogin(loginData) {
+ // create a new login
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ login.init(loginData.hostname,
+ loginData.formSubmitURL || (typeof(loginData.httpRealm) == "string" ? null : ""),
+ typeof(loginData.httpRealm) == "string" ? loginData.httpRealm : null,
+ loginData.username,
+ loginData.password,
+ loginData.usernameElement || "",
+ loginData.passwordElement || "");
+
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.timeCreated = loginData.timeCreated;
+ login.timeLastUsed = loginData.timeLastUsed || loginData.timeCreated;
+ login.timePasswordChanged = loginData.timePasswordChanged || loginData.timeCreated;
+ login.timesUsed = loginData.timesUsed || 1;
+ // While here we're passing formSubmitURL and httpRealm, they could be empty/null and get
+ // ignored in that case, leading to multiple logins for the same username.
+ let existingLogins = Services.logins.findLogins({}, login.hostname,
+ login.formSubmitURL,
+ login.httpRealm);
+ // Check for an existing login that matches *including* the password.
+ // If such a login exists, we do not need to add a new login.
+ if (existingLogins.some(l => login.matches(l, false /* ignorePassword */))) {
+ return null;
+ }
+ // Now check for a login with the same username, where it may be that we have an
+ // updated password.
+ let foundMatchingLogin = false;
+ for (let existingLogin of existingLogins) {
+ if (login.username == existingLogin.username) {
+ foundMatchingLogin = true;
+ existingLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ if (login.password != existingLogin.password &
+ login.timePasswordChanged > existingLogin.timePasswordChanged) {
+ // if a login with the same username and different password already exists and it's older
+ // than the current one, update its password and timestamp.
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ propBag.setProperty("password", login.password);
+ propBag.setProperty("timePasswordChanged", login.timePasswordChanged);
+ Services.logins.modifyLogin(existingLogin, propBag);
+ }
+ }
+ }
+ // if the new login is an update or is older than an exiting login, don't add it.
+ if (foundMatchingLogin) {
+ return null;
+ }
+ return Services.logins.addLogin(login);
+ },
+
+ /**
+ * Convert an array of nsILoginInfo to vanilla JS objects suitable for
+ * sending over IPC.
+ *
+ * NB: All members of nsILoginInfo and nsILoginMetaInfo are strings.
+ */
+ loginsToVanillaObjects(logins) {
+ return logins.map(this.loginToVanillaObject);
+ },
+
+ /**
+ * Same as above, but for a single login.
+ */
+ loginToVanillaObject(login) {
+ let obj = {};
+ for (let i in login.QueryInterface(Ci.nsILoginMetaInfo)) {
+ if (typeof login[i] !== 'function') {
+ obj[i] = login[i];
+ }
+ }
+
+ return obj;
+ },
+
+ /**
+ * Convert an object received from IPC into an nsILoginInfo (with guid).
+ */
+ vanillaObjectToLogin(login) {
+ let formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ formLogin.init(login.hostname, login.formSubmitURL,
+ login.httpRealm, login.username,
+ login.password, login.usernameField,
+ login.passwordField);
+
+ formLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ for (let prop of ["guid", "timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
+ formLogin[prop] = login[prop];
+ }
+ return formLogin;
+ },
+
+ /**
+ * As above, but for an array of objects.
+ */
+ vanillaObjectsToLogins(logins) {
+ return logins.map(this.vanillaObjectToLogin);
+ },
+
+ removeLegacySignonFiles() {
+ const {Constants, Path, File} = Cu.import("resource://gre/modules/osfile.jsm").OS;
+
+ const profileDir = Constants.Path.profileDir;
+ const defaultSignonFilePrefs = new Map([
+ ["signon.SignonFileName", "signons.txt"],
+ ["signon.SignonFileName2", "signons2.txt"],
+ ["signon.SignonFileName3", "signons3.txt"]
+ ]);
+ const toDeletes = new Set();
+
+ for (let [pref, val] of defaultSignonFilePrefs.entries()) {
+ toDeletes.add(Path.join(profileDir, val));
+
+ try {
+ let signonFile = Services.prefs.getCharPref(pref);
+
+ toDeletes.add(Path.join(profileDir, signonFile));
+ Services.prefs.clearUserPref(pref);
+ } catch (e) {}
+ }
+
+ for (let file of toDeletes) {
+ File.remove(file);
+ }
+ },
+
+ /**
+ * Returns true if the user has a master password set and false otherwise.
+ */
+ isMasterPasswordSet() {
+ let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].
+ getService(Ci.nsIPKCS11ModuleDB);
+ let slot = secmodDB.findSlotByName("");
+ if (!slot) {
+ return false;
+ }
+ let hasMP = slot.status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED &&
+ slot.status != Ci.nsIPKCS11Slot.SLOT_READY;
+ return hasMP;
+ },
+
+ /**
+ * Send a notification when stored data is changed.
+ */
+ notifyStorageChanged(changeType, data) {
+ let dataObject = data;
+ // Can't pass a raw JS string or array though notifyObservers(). :-(
+ if (Array.isArray(data)) {
+ dataObject = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ for (let i = 0; i < data.length; i++) {
+ dataObject.appendElement(data[i], false);
+ }
+ } else if (typeof(data) == "string") {
+ dataObject = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ dataObject.data = data;
+ }
+ Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
+ }
+};
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("LoginHelper");
+ return logger;
+});
diff --git a/toolkit/components/passwordmgr/LoginImport.jsm b/toolkit/components/passwordmgr/LoginImport.jsm
new file mode 100644
index 000000000..a1d5c988a
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginImport.jsm
@@ -0,0 +1,173 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Provides an object that has a method to import login-related data from the
+ * previous SQLite storage format.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginImport",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+// LoginImport
+
+/**
+ * Provides an object that has a method to import login-related data from the
+ * previous SQLite storage format.
+ *
+ * @param aStore
+ * LoginStore object where imported data will be added.
+ * @param aPath
+ * String containing the file path of the SQLite login database.
+ */
+this.LoginImport = function (aStore, aPath) {
+ this.store = aStore;
+ this.path = aPath;
+};
+
+this.LoginImport.prototype = {
+ /**
+ * LoginStore object where imported data will be added.
+ */
+ store: null,
+
+ /**
+ * String containing the file path of the SQLite login database.
+ */
+ path: null,
+
+ /**
+ * Imports login-related data from the previous SQLite storage format.
+ */
+ import: Task.async(function* () {
+ // We currently migrate data directly from the database to the JSON store at
+ // first run, then we set a preference to prevent repeating the import.
+ // Thus, merging with existing data is not a use case we support. This
+ // restriction might be removed to support re-importing passwords set by an
+ // old version by flipping the import preference and restarting.
+ if (this.store.data.logins.length > 0 ||
+ this.store.data.disabledHosts.length > 0) {
+ throw new Error("Unable to import saved passwords because some data " +
+ "has already been imported or saved.");
+ }
+
+ // When a timestamp is not specified, we will use the same reference time.
+ let referenceTimeMs = Date.now();
+
+ let connection = yield Sqlite.openConnection({ path: this.path });
+ try {
+ let schemaVersion = yield connection.getSchemaVersion();
+
+ // We support importing database schema versions from 3 onwards.
+ // Version 3 was implemented in bug 316084 (Firefox 3.6, March 2009).
+ // Version 4 was implemented in bug 465636 (Firefox 4, March 2010).
+ // Version 5 was implemented in bug 718817 (Firefox 13, February 2012).
+ if (schemaVersion < 3) {
+ throw new Error("Unable to import saved passwords because " +
+ "the existing profile is too old.");
+ }
+
+ let rows = yield connection.execute("SELECT * FROM moz_logins");
+ for (let row of rows) {
+ try {
+ let hostname = row.getResultByName("hostname");
+ let httpRealm = row.getResultByName("httpRealm");
+ let formSubmitURL = row.getResultByName("formSubmitURL");
+ let usernameField = row.getResultByName("usernameField");
+ let passwordField = row.getResultByName("passwordField");
+ let encryptedUsername = row.getResultByName("encryptedUsername");
+ let encryptedPassword = row.getResultByName("encryptedPassword");
+
+ // The "guid" field was introduced in schema version 2, and the
+ // "enctype" field was introduced in schema version 3. We don't
+ // support upgrading from older versions of the database.
+ let guid = row.getResultByName("guid");
+ let encType = row.getResultByName("encType");
+
+ // The time and count fields were introduced in schema version 4.
+ let timeCreated = null;
+ let timeLastUsed = null;
+ let timePasswordChanged = null;
+ let timesUsed = null;
+ try {
+ timeCreated = row.getResultByName("timeCreated");
+ timeLastUsed = row.getResultByName("timeLastUsed");
+ timePasswordChanged = row.getResultByName("timePasswordChanged");
+ timesUsed = row.getResultByName("timesUsed");
+ } catch (ex) { }
+
+ // These columns may be null either because they were not present in
+ // the database or because the record was created on a new schema
+ // version by an old application version.
+ if (!timeCreated) {
+ timeCreated = referenceTimeMs;
+ }
+ if (!timeLastUsed) {
+ timeLastUsed = referenceTimeMs;
+ }
+ if (!timePasswordChanged) {
+ timePasswordChanged = referenceTimeMs;
+ }
+ if (!timesUsed) {
+ timesUsed = 1;
+ }
+
+ this.store.data.logins.push({
+ id: this.store.data.nextId++,
+ hostname: hostname,
+ httpRealm: httpRealm,
+ formSubmitURL: formSubmitURL,
+ usernameField: usernameField,
+ passwordField: passwordField,
+ encryptedUsername: encryptedUsername,
+ encryptedPassword: encryptedPassword,
+ guid: guid,
+ encType: encType,
+ timeCreated: timeCreated,
+ timeLastUsed: timeLastUsed,
+ timePasswordChanged: timePasswordChanged,
+ timesUsed: timesUsed,
+ });
+ } catch (ex) {
+ Cu.reportError("Error importing login: " + ex);
+ }
+ }
+
+ rows = yield connection.execute("SELECT * FROM moz_disabledHosts");
+ for (let row of rows) {
+ try {
+ let hostname = row.getResultByName("hostname");
+
+ this.store.data.disabledHosts.push(hostname);
+ } catch (ex) {
+ Cu.reportError("Error importing disabled host: " + ex);
+ }
+ }
+ } finally {
+ yield connection.close();
+ }
+ }),
+};
diff --git a/toolkit/components/passwordmgr/LoginManagerContent.jsm b/toolkit/components/passwordmgr/LoginManagerContent.jsm
new file mode 100644
index 000000000..60805530d
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -0,0 +1,1619 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
+ "LoginFormFactory",
+ "UserAutoCompleteResult" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
+const AUTOCOMPLETE_AFTER_CONTEXTMENU_THRESHOLD_MS = 250;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Cu.import("resource://gre/modules/InsecurePasswordUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
+ "resource://gre/modules/FormLikeFactory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
+ "resource://gre/modules/LoginRecipes.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+ "resource://gre/modules/InsecurePasswordUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gNetUtil",
+ "@mozilla.org/network/util;1",
+ "nsINetUtil");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("LoginManagerContent");
+ return logger.log.bind(logger);
+});
+
+// These mirror signon.* prefs.
+var gEnabled, gAutofillForms, gStoreWhenAutocompleteOff;
+var gLastContextMenuEventTimeStamp = Number.NEGATIVE_INFINITY;
+
+var observer = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIFormSubmitObserver,
+ Ci.nsIWebProgressListener,
+ Ci.nsIDOMEventListener,
+ Ci.nsISupportsWeakReference]),
+
+ // nsIFormSubmitObserver
+ notify(formElement, aWindow, actionURI) {
+ log("observer notified for form submission.");
+
+ // We're invoked before the content's |onsubmit| handlers, so we
+ // can grab form data before it might be modified (see bug 257781).
+
+ try {
+ let formLike = LoginFormFactory.createFromForm(formElement);
+ LoginManagerContent._onFormSubmit(formLike);
+ } catch (e) {
+ log("Caught error in onFormSubmit(", e.lineNumber, "):", e.message);
+ Cu.reportError(e);
+ }
+
+ return true; // Always return true, or form submit will be canceled.
+ },
+
+ onPrefChange() {
+ gEnabled = Services.prefs.getBoolPref("signon.rememberSignons");
+ gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms");
+ gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
+ },
+
+ // nsIWebProgressListener
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // Only handle pushState/replaceState here.
+ if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ||
+ !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) {
+ return;
+ }
+
+ log("onLocationChange handled:", aLocation.spec, aWebProgress.DOMWindow.document);
+
+ LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
+ },
+
+ onStateChange(aWebProgress, aRequest, aState, aStatus) {
+ if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
+ return;
+ }
+
+ // We only care about when a page triggered a load, not the user. For example:
+ // clicking refresh/back/forward, typing a URL and hitting enter, and loading a bookmark aren't
+ // likely to be when a user wants to save a login.
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ if (triggeringPrincipal.isNullPrincipal ||
+ triggeringPrincipal.equals(Services.scriptSecurityManager.getSystemPrincipal())) {
+ return;
+ }
+
+ // Don't handle history navigation, reload, or pushState not triggered via chrome UI.
+ // e.g. history.go(-1), location.reload(), history.replaceState()
+ if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) {
+ log("onStateChange: loadType isn't LOAD_CMD_NORMAL:", aWebProgress.loadType);
+ return;
+ }
+
+ log("onStateChange handled:", channel);
+ LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
+ },
+
+ handleEvent(aEvent) {
+ if (!aEvent.isTrusted) {
+ return;
+ }
+
+ if (!gEnabled) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ // Only used for username fields.
+ case "focus": {
+ LoginManagerContent._onUsernameFocus(aEvent);
+ break;
+ }
+
+ case "contextmenu": {
+ gLastContextMenuEventTimeStamp = Date.now();
+ break;
+ }
+
+ default: {
+ throw new Error("Unexpected event");
+ }
+ }
+ },
+};
+
+Services.obs.addObserver(observer, "earlyformsubmit", false);
+var prefBranch = Services.prefs.getBranch("signon.");
+prefBranch.addObserver("", observer.onPrefChange, false);
+
+observer.onPrefChange(); // read initial values
+
+
+function messageManagerFromWindow(win) {
+ return win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+}
+
+// This object maps to the "child" process (even in the single-process case).
+var LoginManagerContent = {
+
+ __formFillService : null, // FormFillController, for username autocompleting
+ get _formFillService() {
+ if (!this.__formFillService)
+ this.__formFillService =
+ Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(Ci.nsIFormFillController);
+ return this.__formFillService;
+ },
+
+ _getRandomId() {
+ return Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ },
+
+ _messages: [ "RemoteLogins:loginsFound",
+ "RemoteLogins:loginsAutoCompleted" ],
+
+ /**
+ * WeakMap of the root element of a FormLike to the FormLike representing its fields.
+ *
+ * This is used to be able to lookup an existing FormLike for a given root element since multiple
+ * calls to LoginFormFactory won't give the exact same object. When batching fills we don't always
+ * want to use the most recent list of elements for a FormLike since we may end up doing multiple
+ * fills for the same set of elements when a field gets added between arming and running the
+ * DeferredTask.
+ *
+ * @type {WeakMap}
+ */
+ _formLikeByRootElement: new WeakMap(),
+
+ /**
+ * WeakMap of the root element of a WeakMap to the DeferredTask to fill its fields.
+ *
+ * This is used to be able to throttle fills for a FormLike since onDOMInputPasswordAdded gets
+ * dispatched for each password field added to a document but we only want to fill once per
+ * FormLike when multiple fields are added at once.
+ *
+ * @type {WeakMap}
+ */
+ _deferredPasswordAddedTasksByRootElement: new WeakMap(),
+
+ // Map from form login requests to information about that request.
+ _requests: new Map(),
+
+ // Number of outstanding requests to each manager.
+ _managers: new Map(),
+
+ _takeRequest(msg) {
+ let data = msg.data;
+ let request = this._requests.get(data.requestId);
+
+ this._requests.delete(data.requestId);
+
+ let count = this._managers.get(msg.target);
+ if (--count === 0) {
+ this._managers.delete(msg.target);
+
+ for (let message of this._messages)
+ msg.target.removeMessageListener(message, this);
+ } else {
+ this._managers.set(msg.target, count);
+ }
+
+ return request;
+ },
+
+ _sendRequest(messageManager, requestData,
+ name, messageData) {
+ let count;
+ if (!(count = this._managers.get(messageManager))) {
+ this._managers.set(messageManager, 1);
+
+ for (let message of this._messages)
+ messageManager.addMessageListener(message, this);
+ } else {
+ this._managers.set(messageManager, ++count);
+ }
+
+ let requestId = this._getRandomId();
+ messageData.requestId = requestId;
+
+ messageManager.sendAsyncMessage(name, messageData);
+
+ let deferred = Promise.defer();
+ requestData.promise = deferred;
+ this._requests.set(requestId, requestData);
+ return deferred.promise;
+ },
+
+ receiveMessage(msg, window) {
+ if (msg.name == "RemoteLogins:fillForm") {
+ this.fillForm({
+ topDocument: window.document,
+ loginFormOrigin: msg.data.loginFormOrigin,
+ loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
+ recipes: msg.data.recipes,
+ inputElement: msg.objects.inputElement,
+ });
+ return;
+ }
+
+ let request = this._takeRequest(msg);
+ switch (msg.name) {
+ case "RemoteLogins:loginsFound": {
+ let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
+ request.promise.resolve({
+ form: request.form,
+ loginsFound: loginsFound,
+ recipes: msg.data.recipes,
+ });
+ break;
+ }
+
+ case "RemoteLogins:loginsAutoCompleted": {
+ let loginsFound =
+ LoginHelper.vanillaObjectsToLogins(msg.data.logins);
+ // If we're in the parent process, don't pass a message manager so our
+ // autocomplete result objects know they can remove the login from the
+ // login manager directly.
+ let messageManager =
+ (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) ?
+ msg.target : undefined;
+ request.promise.resolve({ logins: loginsFound, messageManager });
+ break;
+ }
+ }
+ },
+
+ /**
+ * Get relevant logins and recipes from the parent
+ *
+ * @param {HTMLFormElement} form - form to get login data for
+ * @param {Object} options
+ * @param {boolean} options.showMasterPassword - whether to show a master password prompt
+ */
+ _getLoginDataFromParent(form, options) {
+ let doc = form.ownerDocument;
+ let win = doc.defaultView;
+
+ let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
+ if (!formOrigin) {
+ return Promise.reject("_getLoginDataFromParent: A form origin is required");
+ }
+ let actionOrigin = LoginUtils._getActionOrigin(form);
+
+ let messageManager = messageManagerFromWindow(win);
+
+ // XXX Weak??
+ let requestData = { form: form };
+ let messageData = { formOrigin: formOrigin,
+ actionOrigin: actionOrigin,
+ options: options };
+
+ return this._sendRequest(messageManager, requestData,
+ "RemoteLogins:findLogins",
+ messageData);
+ },
+
+ _autoCompleteSearchAsync(aSearchString, aPreviousResult,
+ aElement, aRect) {
+ let doc = aElement.ownerDocument;
+ let form = LoginFormFactory.createFromField(aElement);
+ let win = doc.defaultView;
+
+ let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
+ let actionOrigin = LoginUtils._getActionOrigin(form);
+
+ let messageManager = messageManagerFromWindow(win);
+
+ let remote = (Services.appinfo.processType ===
+ Services.appinfo.PROCESS_TYPE_CONTENT);
+
+ let previousResult = aPreviousResult ?
+ { searchString: aPreviousResult.searchString,
+ logins: LoginHelper.loginsToVanillaObjects(aPreviousResult.logins) } :
+ null;
+
+ let requestData = {};
+ let messageData = { formOrigin: formOrigin,
+ actionOrigin: actionOrigin,
+ searchString: aSearchString,
+ previousResult: previousResult,
+ rect: aRect,
+ isSecure: InsecurePasswordUtils.isFormSecure(form),
+ isPasswordField: aElement.type == "password",
+ remote: remote };
+
+ return this._sendRequest(messageManager, requestData,
+ "RemoteLogins:autoCompleteLogins",
+ messageData);
+ },
+
+ setupProgressListener(window) {
+ if (!LoginHelper.formlessCaptureEnabled) {
+ return;
+ }
+
+ try {
+ let webProgress = window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShell).
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(observer,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+ } catch (ex) {
+ // Ignore NS_ERROR_FAILURE if the progress listener was already added
+ }
+ },
+
+ onDOMFormHasPassword(event, window) {
+ if (!event.isTrusted) {
+ return;
+ }
+
+ let form = event.target;
+ let formLike = LoginFormFactory.createFromForm(form);
+ log("onDOMFormHasPassword:", form, formLike);
+ this._fetchLoginsFromParentAndFillForm(formLike, window);
+ },
+
+ onDOMInputPasswordAdded(event, window) {
+ if (!event.isTrusted) {
+ return;
+ }
+
+ let pwField = event.target;
+ if (pwField.form) {
+ // Fill is handled by onDOMFormHasPassword which is already throttled.
+ return;
+ }
+
+ // Only setup the listener for formless inputs.
+ // Capture within a <form> but without a submit event is bug 1287202.
+ this.setupProgressListener(window);
+
+ let formLike = LoginFormFactory.createFromField(pwField);
+ log("onDOMInputPasswordAdded:", pwField, formLike);
+
+ let deferredTask = this._deferredPasswordAddedTasksByRootElement.get(formLike.rootElement);
+ if (!deferredTask) {
+ log("Creating a DeferredTask to call _fetchLoginsFromParentAndFillForm soon");
+ this._formLikeByRootElement.set(formLike.rootElement, formLike);
+
+ deferredTask = new DeferredTask(function* deferredInputProcessing() {
+ // Get the updated formLike instead of the one at the time of creating the DeferredTask via
+ // a closure since it could be stale since FormLike.elements isn't live.
+ let formLike2 = this._formLikeByRootElement.get(formLike.rootElement);
+ log("Running deferred processing of onDOMInputPasswordAdded", formLike2);
+ this._deferredPasswordAddedTasksByRootElement.delete(formLike2.rootElement);
+ this._fetchLoginsFromParentAndFillForm(formLike2, window);
+ }.bind(this), PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS);
+
+ this._deferredPasswordAddedTasksByRootElement.set(formLike.rootElement, deferredTask);
+ }
+
+ if (deferredTask.isArmed) {
+ log("DeferredTask is already armed so just updating the FormLike");
+ // We update the FormLike so it (most important .elements) is fresh when the task eventually
+ // runs since changes to the elements could affect our field heuristics.
+ this._formLikeByRootElement.set(formLike.rootElement, formLike);
+ } else if (window.document.readyState == "complete") {
+ log("Arming the DeferredTask we just created since document.readyState == 'complete'");
+ deferredTask.arm();
+ } else {
+ window.addEventListener("DOMContentLoaded", function armPasswordAddedTask() {
+ window.removeEventListener("DOMContentLoaded", armPasswordAddedTask);
+ log("Arming the onDOMInputPasswordAdded DeferredTask due to DOMContentLoaded");
+ deferredTask.arm();
+ });
+ }
+ },
+
+ /**
+ * Fetch logins from the parent for a given form and then attempt to fill it.
+ *
+ * @param {FormLike} form to fetch the logins for then try autofill.
+ * @param {Window} window
+ */
+ _fetchLoginsFromParentAndFillForm(form, window) {
+ this._detectInsecureFormLikes(window);
+
+ let messageManager = messageManagerFromWindow(window);
+ messageManager.sendAsyncMessage("LoginStats:LoginEncountered");
+
+ if (!gEnabled) {
+ return;
+ }
+
+ this._getLoginDataFromParent(form, { showMasterPassword: true })
+ .then(this.loginsFound.bind(this))
+ .then(null, Cu.reportError);
+ },
+
+ onPageShow(event, window) {
+ this._detectInsecureFormLikes(window);
+ },
+
+ /**
+ * Maps all DOM content documents in this content process, including those in
+ * frames, to the current state used by the Login Manager.
+ */
+ loginFormStateByDocument: new WeakMap(),
+
+ /**
+ * Retrieves a reference to the state object associated with the given
+ * document. This is initialized to an object with default values.
+ */
+ stateForDocument(document) {
+ let loginFormState = this.loginFormStateByDocument.get(document);
+ if (!loginFormState) {
+ loginFormState = {
+ /**
+ * Keeps track of filled fields and values.
+ */
+ fillsByRootElement: new WeakMap(),
+ loginFormRootElements: new Set(),
+ };
+ this.loginFormStateByDocument.set(document, loginFormState);
+ }
+ return loginFormState;
+ },
+
+ /**
+ * Compute whether there is an insecure login form on any frame of the current page, and
+ * notify the parent process. This is used to control whether insecure password UI appears.
+ */
+ _detectInsecureFormLikes(topWindow) {
+ log("_detectInsecureFormLikes", topWindow.location.href);
+
+ // Returns true if this window or any subframes have insecure login forms.
+ let hasInsecureLoginForms = (thisWindow) => {
+ let doc = thisWindow.document;
+ let hasLoginForm = this.stateForDocument(doc).loginFormRootElements.size > 0;
+ // Ignores window.opener, because it's not relevant for indicating
+ // form security. See InsecurePasswordUtils docs for details.
+ return (hasLoginForm && !thisWindow.isSecureContextIfOpenerIgnored) ||
+ Array.some(thisWindow.frames,
+ frame => hasInsecureLoginForms(frame));
+ };
+
+ let messageManager = messageManagerFromWindow(topWindow);
+ messageManager.sendAsyncMessage("RemoteLogins:insecureLoginFormPresent", {
+ hasInsecureLoginForms: hasInsecureLoginForms(topWindow),
+ });
+ },
+
+ /**
+ * Perform a password fill upon user request coming from the parent process.
+ * The fill will be in the form previously identified during page navigation.
+ *
+ * @param An object with the following properties:
+ * {
+ * topDocument:
+ * DOM document currently associated to the the top-level window
+ * for which the fill is requested. This may be different from the
+ * document that originally caused the login UI to be displayed.
+ * loginFormOrigin:
+ * String with the origin for which the login UI was displayed.
+ * This must match the origin of the form used for the fill.
+ * loginsFound:
+ * Array containing the login to fill. While other messages may
+ * have more logins, for this use case this is expected to have
+ * exactly one element. The origin of the login may be different
+ * from the origin of the form used for the fill.
+ * recipes:
+ * Fill recipes transmitted together with the original message.
+ * inputElement:
+ * Username or password input element from the form we want to fill.
+ * }
+ */
+ fillForm({ topDocument, loginFormOrigin, loginsFound, recipes, inputElement }) {
+ if (!inputElement) {
+ log("fillForm: No input element specified");
+ return;
+ }
+ if (LoginUtils._getPasswordOrigin(topDocument.documentURI) != loginFormOrigin) {
+ if (!inputElement ||
+ LoginUtils._getPasswordOrigin(inputElement.ownerDocument.documentURI) != loginFormOrigin) {
+ log("fillForm: The requested origin doesn't match the one form the",
+ "document. This may mean we navigated to a document from a different",
+ "site before we had a chance to indicate this change in the user",
+ "interface.");
+ return;
+ }
+ }
+
+ let clobberUsername = true;
+ let options = {
+ inputElement,
+ };
+
+ let form = LoginFormFactory.createFromField(inputElement);
+ if (inputElement.type == "password") {
+ clobberUsername = false;
+ }
+ this._fillForm(form, true, clobberUsername, true, true, loginsFound, recipes, options);
+ },
+
+ loginsFound({ form, loginsFound, recipes }) {
+ let doc = form.ownerDocument;
+ let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView);
+
+ this._fillForm(form, autofillForm, false, false, false, loginsFound, recipes);
+ },
+
+ /**
+ * Focus event handler for username fields to decide whether to show autocomplete.
+ * @param {FocusEvent} event
+ */
+ _onUsernameFocus(event) {
+ let focusedField = event.target;
+ if (!focusedField.mozIsTextField(true) || focusedField.readOnly) {
+ return;
+ }
+
+ if (this._isLoginAlreadyFilled(focusedField)) {
+ log("_onUsernameFocus: Already filled");
+ return;
+ }
+
+ /*
+ * A `focus` event is fired before a `contextmenu` event if a user right-clicks into an
+ * unfocused field. In that case we don't want to show both autocomplete and a context menu
+ * overlapping so we spin the event loop to see if a `contextmenu` event is coming next. If no
+ * `contextmenu` event was seen and the focused field is still focused by the form fill
+ * controller then show the autocomplete popup.
+ */
+ let timestamp = Date.now();
+ setTimeout(function maybeOpenAutocompleteAfterFocus() {
+ // Even though the `focus` event happens first, its .timeStamp is greater in
+ // testing and I don't want to rely on that so the absolute value is used.
+ let timeDiff = Math.abs(gLastContextMenuEventTimeStamp - timestamp);
+ if (timeDiff < AUTOCOMPLETE_AFTER_CONTEXTMENU_THRESHOLD_MS) {
+ log("Not opening autocomplete after focus since a context menu was opened within",
+ timeDiff, "ms");
+ return;
+ }
+
+ if (this._formFillService.focusedInput == focusedField) {
+ log("maybeOpenAutocompleteAfterFocus: Opening the autocomplete popup. Time diff:", timeDiff);
+ this._formFillService.showPopup();
+ } else {
+ log("maybeOpenAutocompleteAfterFocus: FormFillController has a different focused input");
+ }
+ }.bind(this), 0);
+ },
+
+ /**
+ * Listens for DOMAutoComplete and blur events on an input field.
+ */
+ onUsernameInput(event) {
+ if (!event.isTrusted)
+ return;
+
+ if (!gEnabled)
+ return;
+
+ var acInputField = event.target;
+
+ // This is probably a bit over-conservatative.
+ if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument))
+ return;
+
+ if (!LoginHelper.isUsernameFieldType(acInputField))
+ return;
+
+ var acForm = LoginFormFactory.createFromField(acInputField);
+ if (!acForm)
+ return;
+
+ // If the username is blank, bail out now -- we don't want
+ // fillForm() to try filling in a login without a username
+ // to filter on (bug 471906).
+ if (!acInputField.value)
+ return;
+
+ log("onUsernameInput from", event.type);
+
+ let doc = acForm.ownerDocument;
+ let messageManager = messageManagerFromWindow(doc.defaultView);
+ let recipes = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+ formOrigin: LoginUtils._getPasswordOrigin(doc.documentURI),
+ })[0];
+
+ // Make sure the username field fillForm will use is the
+ // same field as the autocomplete was activated on.
+ var [usernameField, passwordField, ignored] =
+ this._getFormFields(acForm, false, recipes);
+ if (usernameField == acInputField && passwordField) {
+ this._getLoginDataFromParent(acForm, { showMasterPassword: false })
+ .then(({ form, loginsFound, recipes }) => {
+ this._fillForm(form, true, false, true, true, loginsFound, recipes);
+ })
+ .then(null, Cu.reportError);
+ } else {
+ // Ignore the event, it's for some input we don't care about.
+ }
+ },
+
+ /**
+ * @param {FormLike} form - the FormLike to look for password fields in.
+ * @param {bool} [skipEmptyFields=false] - Whether to ignore password fields with no value.
+ * Used at capture time since saving empty values isn't
+ * useful.
+ * @return {Array|null} Array of password field elements for the specified form.
+ * If no pw fields are found, or if more than 3 are found, then null
+ * is returned.
+ */
+ _getPasswordFields(form, skipEmptyFields = false) {
+ // Locate the password fields in the form.
+ let pwFields = [];
+ for (let i = 0; i < form.elements.length; i++) {
+ let element = form.elements[i];
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
+ element.type != "password") {
+ continue;
+ }
+
+ if (skipEmptyFields && !element.value.trim()) {
+ continue;
+ }
+
+ pwFields[pwFields.length] = {
+ index : i,
+ element : element
+ };
+ }
+
+ // If too few or too many fields, bail out.
+ if (pwFields.length == 0) {
+ log("(form ignored -- no password fields.)");
+ return null;
+ } else if (pwFields.length > 3) {
+ log("(form ignored -- too many password fields. [ got ", pwFields.length, "])");
+ return null;
+ }
+
+ return pwFields;
+ },
+
+ /**
+ * Returns the username and password fields found in the form.
+ * Can handle complex forms by trying to figure out what the
+ * relevant fields are.
+ *
+ * @param {FormLike} form
+ * @param {bool} isSubmission
+ * @param {Set} recipes
+ * @return {Array} [usernameField, newPasswordField, oldPasswordField]
+ *
+ * usernameField may be null.
+ * newPasswordField will always be non-null.
+ * oldPasswordField may be null. If null, newPasswordField is just
+ * "theLoginField". If not null, the form is apparently a
+ * change-password field, with oldPasswordField containing the password
+ * that is being changed.
+ *
+ * Note that even though we can create a FormLike from a text field,
+ * this method will only return a non-null usernameField if the
+ * FormLike has a password field.
+ */
+ _getFormFields(form, isSubmission, recipes) {
+ var usernameField = null;
+ var pwFields = null;
+ var fieldOverrideRecipe = LoginRecipesContent.getFieldOverrides(recipes, form);
+ if (fieldOverrideRecipe) {
+ var pwOverrideField = LoginRecipesContent.queryLoginField(
+ form,
+ fieldOverrideRecipe.passwordSelector
+ );
+ if (pwOverrideField) {
+ // The field from the password override may be in a different FormLike.
+ let formLike = LoginFormFactory.createFromField(pwOverrideField);
+ pwFields = [{
+ index : [...formLike.elements].indexOf(pwOverrideField),
+ element : pwOverrideField,
+ }];
+ }
+
+ var usernameOverrideField = LoginRecipesContent.queryLoginField(
+ form,
+ fieldOverrideRecipe.usernameSelector
+ );
+ if (usernameOverrideField) {
+ usernameField = usernameOverrideField;
+ }
+ }
+
+ if (!pwFields) {
+ // Locate the password field(s) in the form. Up to 3 supported.
+ // If there's no password field, there's nothing for us to do.
+ pwFields = this._getPasswordFields(form, isSubmission);
+ }
+
+ if (!pwFields) {
+ return [null, null, null];
+ }
+
+ if (!usernameField) {
+ // Locate the username field in the form by searching backwards
+ // from the first password field, assume the first text field is the
+ // username. We might not find a username field if the user is
+ // already logged in to the site.
+ for (var i = pwFields[0].index - 1; i >= 0; i--) {
+ var element = form.elements[i];
+ if (!LoginHelper.isUsernameFieldType(element)) {
+ continue;
+ }
+
+ if (fieldOverrideRecipe && fieldOverrideRecipe.notUsernameSelector &&
+ element.matches(fieldOverrideRecipe.notUsernameSelector)) {
+ continue;
+ }
+
+ usernameField = element;
+ break;
+ }
+ }
+
+ if (!usernameField)
+ log("(form -- no username field found)");
+ else
+ log("Username field ", usernameField, "has name/value:",
+ usernameField.name, "/", usernameField.value);
+
+ // If we're not submitting a form (it's a page load), there are no
+ // password field values for us to use for identifying fields. So,
+ // just assume the first password field is the one to be filled in.
+ if (!isSubmission || pwFields.length == 1) {
+ var passwordField = pwFields[0].element;
+ log("Password field", passwordField, "has name: ", passwordField.name);
+ return [usernameField, passwordField, null];
+ }
+
+
+ // Try to figure out WTF is in the form based on the password values.
+ var oldPasswordField, newPasswordField;
+ var pw1 = pwFields[0].element.value;
+ var pw2 = pwFields[1].element.value;
+ var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
+
+ if (pwFields.length == 3) {
+ // Look for two identical passwords, that's the new password
+
+ if (pw1 == pw2 && pw2 == pw3) {
+ // All 3 passwords the same? Weird! Treat as if 1 pw field.
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = null;
+ } else if (pw1 == pw2) {
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = pwFields[2].element;
+ } else if (pw2 == pw3) {
+ oldPasswordField = pwFields[0].element;
+ newPasswordField = pwFields[2].element;
+ } else if (pw1 == pw3) {
+ // A bit odd, but could make sense with the right page layout.
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = pwFields[1].element;
+ } else {
+ // We can't tell which of the 3 passwords should be saved.
+ log("(form ignored -- all 3 pw fields differ)");
+ return [null, null, null];
+ }
+ } else if (pw1 == pw2) {
+ // pwFields.length == 2
+ // Treat as if 1 pw field
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = null;
+ } else {
+ // Just assume that the 2nd password is the new password
+ oldPasswordField = pwFields[0].element;
+ newPasswordField = pwFields[1].element;
+ }
+
+ log("Password field (new) id/name is: ", newPasswordField.id, " / ", newPasswordField.name);
+ if (oldPasswordField) {
+ log("Password field (old) id/name is: ", oldPasswordField.id, " / ", oldPasswordField.name);
+ } else {
+ log("Password field (old):", oldPasswordField);
+ }
+ return [usernameField, newPasswordField, oldPasswordField];
+ },
+
+
+ /**
+ * @return true if the page requests autocomplete be disabled for the
+ * specified element.
+ */
+ _isAutocompleteDisabled(element) {
+ return element && element.autocomplete == "off";
+ },
+
+ /**
+ * Trigger capture on any relevant FormLikes due to a navigation alone (not
+ * necessarily due to an actual form submission). This method is used to
+ * capture logins for cases where form submit events are not used.
+ *
+ * To avoid multiple notifications for the same FormLike, this currently
+ * avoids capturing when dealing with a real <form> which are ideally already
+ * using a submit event.
+ *
+ * @param {Document} document being navigated
+ */
+ _onNavigation(aDocument) {
+ let state = this.stateForDocument(aDocument);
+ let loginFormRootElements = state.loginFormRootElements;
+ log("_onNavigation: state:", state, "loginFormRootElements size:", loginFormRootElements.size,
+ "document:", aDocument);
+
+ for (let formRoot of state.loginFormRootElements) {
+ if (formRoot instanceof Ci.nsIDOMHTMLFormElement) {
+ // For now only perform capture upon navigation for FormLike's without
+ // a <form> to avoid capture from both an earlyformsubmit and
+ // navigation for the same "form".
+ log("Ignoring navigation for the form root to avoid multiple prompts " +
+ "since it was for a real <form>");
+ continue;
+ }
+ let formLike = this._formLikeByRootElement.get(formRoot);
+ this._onFormSubmit(formLike);
+ }
+ },
+
+ /**
+ * Called by our observer when notified of a form submission.
+ * [Note that this happens before any DOM onsubmit handlers are invoked.]
+ * Looks for a password change in the submitted form, so we can update
+ * our stored password.
+ *
+ * @param {FormLike} form
+ */
+ _onFormSubmit(form) {
+ log("_onFormSubmit", form);
+ var doc = form.ownerDocument;
+ var win = doc.defaultView;
+
+ if (PrivateBrowsingUtils.isContentWindowPrivate(win)) {
+ // We won't do anything in private browsing mode anyway,
+ // so there's no need to perform further checks.
+ log("(form submission ignored in private browsing mode)");
+ return;
+ }
+
+ // If password saving is disabled (globally or for host), bail out now.
+ if (!gEnabled)
+ return;
+
+ var hostname = LoginUtils._getPasswordOrigin(doc.documentURI);
+ if (!hostname) {
+ log("(form submission ignored -- invalid hostname)");
+ return;
+ }
+
+ let formSubmitURL = LoginUtils._getActionOrigin(form);
+ let messageManager = messageManagerFromWindow(win);
+
+ let recipes = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+ formOrigin: hostname,
+ })[0];
+
+ // Get the appropriate fields from the form.
+ var [usernameField, newPasswordField, oldPasswordField] =
+ this._getFormFields(form, true, recipes);
+
+ // Need at least 1 valid password field to do anything.
+ if (newPasswordField == null)
+ return;
+
+ // Check for autocomplete=off attribute. We don't use it to prevent
+ // autofilling (for existing logins), but won't save logins when it's
+ // present and the storeWhenAutocompleteOff pref is false.
+ // XXX spin out a bug that we don't update timeLastUsed in this case?
+ if ((this._isAutocompleteDisabled(form) ||
+ this._isAutocompleteDisabled(usernameField) ||
+ this._isAutocompleteDisabled(newPasswordField) ||
+ this._isAutocompleteDisabled(oldPasswordField)) &&
+ !gStoreWhenAutocompleteOff) {
+ log("(form submission ignored -- autocomplete=off found)");
+ return;
+ }
+
+ // Don't try to send DOM nodes over IPC.
+ let mockUsername = usernameField ?
+ { name: usernameField.name,
+ value: usernameField.value } :
+ null;
+ let mockPassword = { name: newPasswordField.name,
+ value: newPasswordField.value };
+ let mockOldPassword = oldPasswordField ?
+ { name: oldPasswordField.name,
+ value: oldPasswordField.value } :
+ null;
+
+ // Make sure to pass the opener's top in case it was in a frame.
+ let openerTopWindow = win.opener ? win.opener.top : null;
+
+ messageManager.sendAsyncMessage("RemoteLogins:onFormSubmit",
+ { hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ usernameField: mockUsername,
+ newPasswordField: mockPassword,
+ oldPasswordField: mockOldPassword },
+ { openerTopWindow });
+ },
+
+ /**
+ * Attempt to find the username and password fields in a form, and fill them
+ * in using the provided logins and recipes.
+ *
+ * @param {LoginForm} form
+ * @param {bool} autofillForm denotes if we should fill the form in automatically
+ * @param {bool} clobberUsername controls if an existing username can be overwritten.
+ * If this is false and an inputElement of type password
+ * is also passed, the username field will be ignored.
+ * If this is false and no inputElement is passed, if the username
+ * field value is not found in foundLogins, it will not fill the password.
+ * @param {bool} clobberPassword controls if an existing password value can be
+ * overwritten
+ * @param {bool} userTriggered is an indication of whether this filling was triggered by
+ * the user
+ * @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form
+ * @param {Set} recipes that could be used to affect how the form is filled
+ * @param {Object} [options = {}] is a list of options for this method.
+ - [inputElement] is an optional target input element we want to fill
+ */
+ _fillForm(form, autofillForm, clobberUsername, clobberPassword,
+ userTriggered, foundLogins, recipes, {inputElement} = {}) {
+ if (form instanceof Ci.nsIDOMHTMLFormElement) {
+ throw new Error("_fillForm should only be called with FormLike objects");
+ }
+
+ log("_fillForm", form.elements);
+ let ignoreAutocomplete = true;
+ // Will be set to one of AUTOFILL_RESULT in the `try` block.
+ let autofillResult = -1;
+ const AUTOFILL_RESULT = {
+ FILLED: 0,
+ NO_PASSWORD_FIELD: 1,
+ PASSWORD_DISABLED_READONLY: 2,
+ NO_LOGINS_FIT: 3,
+ NO_SAVED_LOGINS: 4,
+ EXISTING_PASSWORD: 5,
+ EXISTING_USERNAME: 6,
+ MULTIPLE_LOGINS: 7,
+ NO_AUTOFILL_FORMS: 8,
+ AUTOCOMPLETE_OFF: 9,
+ INSECURE: 10,
+ };
+
+ try {
+ // Nothing to do if we have no matching logins available,
+ // and there isn't a need to show the insecure form warning.
+ if (foundLogins.length == 0 &&
+ (InsecurePasswordUtils.isFormSecure(form) ||
+ !LoginHelper.showInsecureFieldWarning)) {
+ // We don't log() here since this is a very common case.
+ autofillResult = AUTOFILL_RESULT.NO_SAVED_LOGINS;
+ return;
+ }
+
+ // Heuristically determine what the user/pass fields are
+ // We do this before checking to see if logins are stored,
+ // so that the user isn't prompted for a master password
+ // without need.
+ var [usernameField, passwordField, ignored] =
+ this._getFormFields(form, false, recipes);
+
+ // If we have a password inputElement parameter and it's not
+ // the same as the one heuristically found, use the parameter
+ // one instead.
+ if (inputElement) {
+ if (inputElement.type == "password") {
+ passwordField = inputElement;
+ if (!clobberUsername) {
+ usernameField = null;
+ }
+ } else if (LoginHelper.isUsernameFieldType(inputElement)) {
+ usernameField = inputElement;
+ } else {
+ throw new Error("Unexpected input element type.");
+ }
+ }
+
+ // Need a valid password field to do anything.
+ if (passwordField == null) {
+ log("not filling form, no password field found");
+ autofillResult = AUTOFILL_RESULT.NO_PASSWORD_FIELD;
+ return;
+ }
+
+ // If the password field is disabled or read-only, there's nothing to do.
+ if (passwordField.disabled || passwordField.readOnly) {
+ log("not filling form, password field disabled or read-only");
+ autofillResult = AUTOFILL_RESULT.PASSWORD_DISABLED_READONLY;
+ return;
+ }
+
+ // Attach autocomplete stuff to the username field, if we have
+ // one. This is normally used to select from multiple accounts,
+ // but even with one account we should refill if the user edits.
+ // We would also need this attached to show the insecure login
+ // warning, regardless of saved login.
+ if (usernameField) {
+ this._formFillService.markAsLoginManagerField(usernameField);
+ }
+
+ // Nothing to do if we have no matching logins available.
+ // Only insecure pages reach this block and logs the same
+ // telemetry flag.
+ if (foundLogins.length == 0) {
+ // We don't log() here since this is a very common case.
+ autofillResult = AUTOFILL_RESULT.NO_SAVED_LOGINS;
+ return;
+ }
+
+ // Prevent autofilling insecure forms.
+ if (!userTriggered && !LoginHelper.insecureAutofill &&
+ !InsecurePasswordUtils.isFormSecure(form)) {
+ log("not filling form since it's insecure");
+ autofillResult = AUTOFILL_RESULT.INSECURE;
+ return;
+ }
+
+ var isAutocompleteOff = false;
+ if (this._isAutocompleteDisabled(form) ||
+ this._isAutocompleteDisabled(usernameField) ||
+ this._isAutocompleteDisabled(passwordField)) {
+ isAutocompleteOff = true;
+ }
+
+ // Discard logins which have username/password values that don't
+ // fit into the fields (as specified by the maxlength attribute).
+ // The user couldn't enter these values anyway, and it helps
+ // with sites that have an extra PIN to be entered (bug 391514)
+ var maxUsernameLen = Number.MAX_VALUE;
+ var maxPasswordLen = Number.MAX_VALUE;
+
+ // If attribute wasn't set, default is -1.
+ if (usernameField && usernameField.maxLength >= 0)
+ maxUsernameLen = usernameField.maxLength;
+ if (passwordField.maxLength >= 0)
+ maxPasswordLen = passwordField.maxLength;
+
+ var logins = foundLogins.filter(function (l) {
+ var fit = (l.username.length <= maxUsernameLen &&
+ l.password.length <= maxPasswordLen);
+ if (!fit)
+ log("Ignored", l.username, "login: won't fit");
+
+ return fit;
+ }, this);
+
+ if (logins.length == 0) {
+ log("form not filled, none of the logins fit in the field");
+ autofillResult = AUTOFILL_RESULT.NO_LOGINS_FIT;
+ return;
+ }
+
+ // Don't clobber an existing password.
+ if (passwordField.value && !clobberPassword) {
+ log("form not filled, the password field was already filled");
+ autofillResult = AUTOFILL_RESULT.EXISTING_PASSWORD;
+ return;
+ }
+
+ // Select a login to use for filling in the form.
+ var selectedLogin;
+ if (!clobberUsername && usernameField && (usernameField.value ||
+ usernameField.disabled ||
+ usernameField.readOnly)) {
+ // If username was specified in the field, it's disabled or it's readOnly, only fill in the
+ // password if we find a matching login.
+ var username = usernameField.value.toLowerCase();
+
+ let matchingLogins = logins.filter(l =>
+ l.username.toLowerCase() == username);
+ if (matchingLogins.length == 0) {
+ log("Password not filled. None of the stored logins match the username already present.");
+ autofillResult = AUTOFILL_RESULT.EXISTING_USERNAME;
+ return;
+ }
+
+ // If there are multiple, and one matches case, use it
+ for (let l of matchingLogins) {
+ if (l.username == usernameField.value) {
+ selectedLogin = l;
+ }
+ }
+ // Otherwise just use the first
+ if (!selectedLogin) {
+ selectedLogin = matchingLogins[0];
+ }
+ } else if (logins.length == 1) {
+ selectedLogin = logins[0];
+ } else {
+ // We have multiple logins. Handle a special case here, for sites
+ // which have a normal user+pass login *and* a password-only login
+ // (eg, a PIN). Prefer the login that matches the type of the form
+ // (user+pass or pass-only) when there's exactly one that matches.
+ let matchingLogins;
+ if (usernameField)
+ matchingLogins = logins.filter(l => l.username);
+ else
+ matchingLogins = logins.filter(l => !l.username);
+
+ if (matchingLogins.length != 1) {
+ log("Multiple logins for form, so not filling any.");
+ autofillResult = AUTOFILL_RESULT.MULTIPLE_LOGINS;
+ return;
+ }
+
+ selectedLogin = matchingLogins[0];
+ }
+
+ // We will always have a selectedLogin at this point.
+
+ if (!autofillForm) {
+ log("autofillForms=false but form can be filled");
+ autofillResult = AUTOFILL_RESULT.NO_AUTOFILL_FORMS;
+ return;
+ }
+
+ if (isAutocompleteOff && !ignoreAutocomplete) {
+ log("Not filling the login because we're respecting autocomplete=off");
+ autofillResult = AUTOFILL_RESULT.AUTOCOMPLETE_OFF;
+ return;
+ }
+
+ // Fill the form
+
+ if (usernameField) {
+ // Don't modify the username field if it's disabled or readOnly so we preserve its case.
+ let disabledOrReadOnly = usernameField.disabled || usernameField.readOnly;
+
+ let userNameDiffers = selectedLogin.username != usernameField.value;
+ // Don't replace the username if it differs only in case, and the user triggered
+ // this autocomplete. We assume that if it was user-triggered the entered text
+ // is desired.
+ let userEnteredDifferentCase = userTriggered && userNameDiffers &&
+ usernameField.value.toLowerCase() == selectedLogin.username.toLowerCase();
+
+ if (!disabledOrReadOnly && !userEnteredDifferentCase && userNameDiffers) {
+ usernameField.setUserInput(selectedLogin.username);
+ }
+ }
+
+ let doc = form.ownerDocument;
+ if (passwordField.value != selectedLogin.password) {
+ passwordField.setUserInput(selectedLogin.password);
+ let autoFilledLogin = {
+ guid: selectedLogin.QueryInterface(Ci.nsILoginMetaInfo).guid,
+ username: selectedLogin.username,
+ usernameField: usernameField ? Cu.getWeakReference(usernameField) : null,
+ password: selectedLogin.password,
+ passwordField: Cu.getWeakReference(passwordField),
+ };
+ log("Saving autoFilledLogin", autoFilledLogin.guid, "for", form.rootElement);
+ this.stateForDocument(doc).fillsByRootElement.set(form.rootElement, autoFilledLogin);
+ }
+
+ log("_fillForm succeeded");
+ autofillResult = AUTOFILL_RESULT.FILLED;
+
+ let win = doc.defaultView;
+ let messageManager = messageManagerFromWindow(win);
+ messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
+ } finally {
+ if (autofillResult == -1) {
+ // eslint-disable-next-line no-unsafe-finally
+ throw new Error("_fillForm: autofillResult must be specified");
+ }
+
+ if (!userTriggered) {
+ // Ignore fills as a result of user action for this probe.
+ Services.telemetry.getHistogramById("PWMGR_FORM_AUTOFILL_RESULT").add(autofillResult);
+
+ if (usernameField) {
+ let focusedElement = this._formFillService.focusedInput;
+ if (usernameField == focusedElement &&
+ autofillResult !== AUTOFILL_RESULT.FILLED) {
+ log("_fillForm: Opening username autocomplete popup since the form wasn't autofilled");
+ this._formFillService.showPopup();
+ }
+ }
+ }
+
+ if (usernameField) {
+ log("_fillForm: Attaching event listeners to usernameField");
+ usernameField.addEventListener("focus", observer);
+ usernameField.addEventListener("contextmenu", observer);
+ }
+
+ Services.obs.notifyObservers(form.rootElement, "passwordmgr-processed-form", null);
+ }
+ },
+
+ /**
+ * Given a field, determine whether that field was last filled as a username
+ * field AND whether the username is still filled in with the username AND
+ * whether the associated password field has the matching password.
+ *
+ * @note This could possibly be unified with getFieldContext but they have
+ * slightly different use cases. getFieldContext looks up recipes whereas this
+ * method doesn't need to since it's only returning a boolean based upon the
+ * recipes used for the last fill (in _fillForm).
+ *
+ * @param {HTMLInputElement} aUsernameField element contained in a FormLike
+ * cached in _formLikeByRootElement.
+ * @returns {Boolean} whether the username and password fields still have the
+ * last-filled values, if previously filled.
+ */
+ _isLoginAlreadyFilled(aUsernameField) {
+ let formLikeRoot = FormLikeFactory.findRootForField(aUsernameField);
+ // Look for the existing FormLike.
+ let existingFormLike = this._formLikeByRootElement.get(formLikeRoot);
+ if (!existingFormLike) {
+ throw new Error("_isLoginAlreadyFilled called with a username field with " +
+ "no rootElement FormLike");
+ }
+
+ log("_isLoginAlreadyFilled: existingFormLike", existingFormLike);
+ let filledLogin = this.stateForDocument(aUsernameField.ownerDocument).fillsByRootElement.get(formLikeRoot);
+ if (!filledLogin) {
+ return false;
+ }
+
+ // Unpack the weak references.
+ let autoFilledUsernameField = filledLogin.usernameField ? filledLogin.usernameField.get() : null;
+ let autoFilledPasswordField = filledLogin.passwordField.get();
+
+ // Check username and password values match what was filled.
+ if (!autoFilledUsernameField ||
+ autoFilledUsernameField != aUsernameField ||
+ autoFilledUsernameField.value != filledLogin.username ||
+ !autoFilledPasswordField ||
+ autoFilledPasswordField.value != filledLogin.password) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Verify if a field is a valid login form field and
+ * returns some information about it's FormLike.
+ *
+ * @param {Element} aField
+ * A form field we want to verify.
+ *
+ * @returns {Object} an object with information about the
+ * FormLike username and password field
+ * or null if the passed field is invalid.
+ */
+ getFieldContext(aField) {
+ // If the element is not a proper form field, return null.
+ if (!(aField instanceof Ci.nsIDOMHTMLInputElement) ||
+ (aField.type != "password" && !LoginHelper.isUsernameFieldType(aField)) ||
+ !aField.ownerDocument) {
+ return null;
+ }
+ let form = LoginFormFactory.createFromField(aField);
+
+ let doc = aField.ownerDocument;
+ let messageManager = messageManagerFromWindow(doc.defaultView);
+ let recipes = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+ formOrigin: LoginUtils._getPasswordOrigin(doc.documentURI),
+ })[0];
+
+ let [usernameField, newPasswordField] =
+ this._getFormFields(form, false, recipes);
+
+ // If we are not verifying a password field, we want
+ // to use aField as the username field.
+ if (aField.type != "password") {
+ usernameField = aField;
+ }
+
+ return {
+ usernameField: {
+ found: !!usernameField,
+ disabled: usernameField && (usernameField.disabled || usernameField.readOnly),
+ },
+ passwordField: {
+ found: !!newPasswordField,
+ disabled: newPasswordField && (newPasswordField.disabled || newPasswordField.readOnly),
+ },
+ };
+ },
+};
+
+var LoginUtils = {
+ /**
+ * Get the parts of the URL we want for identification.
+ * Strip out things like the userPass portion
+ */
+ _getPasswordOrigin(uriString, allowJS) {
+ var realm = "";
+ try {
+ var uri = Services.io.newURI(uriString, null, null);
+
+ if (allowJS && uri.scheme == "javascript")
+ return "javascript:";
+
+ // Build this manually instead of using prePath to avoid including the userPass portion.
+ realm = uri.scheme + "://" + uri.hostPort;
+ } catch (e) {
+ // bug 159484 - disallow url types that don't support a hostPort.
+ // (although we handle "javascript:..." as a special case above.)
+ log("Couldn't parse origin for", uriString, e);
+ realm = null;
+ }
+
+ return realm;
+ },
+
+ _getActionOrigin(form) {
+ var uriString = form.action;
+
+ // A blank or missing action submits to where it came from.
+ if (uriString == "")
+ uriString = form.baseURI; // ala bug 297761
+
+ return this._getPasswordOrigin(uriString, true);
+ },
+};
+
+// nsIAutoCompleteResult implementation
+function UserAutoCompleteResult(aSearchString, matchingLogins, {isSecure, messageManager, isPasswordField}) {
+ function loginSort(a, b) {
+ var userA = a.username.toLowerCase();
+ var userB = b.username.toLowerCase();
+
+ if (userA < userB)
+ return -1;
+
+ if (userA > userB)
+ return 1;
+
+ return 0;
+ }
+
+ function findDuplicates(loginList) {
+ let seen = new Set();
+ let duplicates = new Set();
+ for (let login of loginList) {
+ if (seen.has(login.username)) {
+ duplicates.add(login.username);
+ }
+ seen.add(login.username);
+ }
+ return duplicates;
+ }
+
+ this._showInsecureFieldWarning = (!isSecure && LoginHelper.showInsecureFieldWarning) ? 1 : 0;
+ this.searchString = aSearchString;
+ this.logins = matchingLogins.sort(loginSort);
+ this.matchCount = matchingLogins.length + this._showInsecureFieldWarning;
+ this._messageManager = messageManager;
+ this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+ this._dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+
+ this._isPasswordField = isPasswordField;
+
+ this._duplicateUsernames = findDuplicates(matchingLogins);
+
+ if (this.matchCount > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ this.defaultIndex = 0;
+ }
+}
+
+UserAutoCompleteResult.prototype = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
+ Ci.nsISupportsWeakReference]),
+
+ // private
+ logins : null,
+
+ // Allow autoCompleteSearch to get at the JS object so it can
+ // modify some readonly properties for internal use.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // Interfaces from idl...
+ searchString : null,
+ searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
+ defaultIndex : -1,
+ errorDescription : "",
+ matchCount : 0,
+
+ getValueAt(index) {
+ if (index < 0 || index >= this.matchCount) {
+ throw new Error("Index out of range.");
+ }
+
+ if (this._showInsecureFieldWarning && index === 0) {
+ return "";
+ }
+
+ let selectedLogin = this.logins[index - this._showInsecureFieldWarning];
+
+ return this._isPasswordField ? selectedLogin.password : selectedLogin.username;
+ },
+
+ getLabelAt(index) {
+ if (index < 0 || index >= this.matchCount) {
+ throw new Error("Index out of range.");
+ }
+
+ if (this._showInsecureFieldWarning && index === 0) {
+ return this._stringBundle.GetStringFromName("insecureFieldWarningDescription") + " " +
+ this._stringBundle.GetStringFromName("insecureFieldWarningLearnMore");
+ }
+
+ let that = this;
+
+ function getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return that._stringBundle.formatStringFromName(key, formatArgs, formatArgs.length);
+ }
+ return that._stringBundle.GetStringFromName(key);
+ }
+
+ let login = this.logins[index - this._showInsecureFieldWarning];
+ let username = login.username;
+ // If login is empty or duplicated we want to append a modification date to it.
+ if (!username || this._duplicateUsernames.has(username)) {
+ if (!username) {
+ username = getLocalizedString("noUsername");
+ }
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ let time = this._dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+ username = getLocalizedString("loginHostAge", [username, time]);
+ }
+
+ return username;
+ },
+
+ getCommentAt(index) {
+ return "";
+ },
+
+ getStyleAt(index) {
+ if (index == 0 && this._showInsecureFieldWarning) {
+ return "insecureWarning";
+ }
+
+ return "login";
+ },
+
+ getImageAt(index) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(index) {
+ return this.getValueAt(index);
+ },
+
+ removeValueAt(index, removeFromDB) {
+ if (index < 0 || index >= this.matchCount) {
+ throw new Error("Index out of range.");
+ }
+
+ if (this._showInsecureFieldWarning && index === 0) {
+ // Ignore the warning message item.
+ return;
+ }
+ if (this._showInsecureFieldWarning) {
+ index--;
+ }
+
+ var [removedLogin] = this.logins.splice(index, 1);
+
+ this.matchCount--;
+ if (this.defaultIndex > this.logins.length)
+ this.defaultIndex--;
+
+ if (removeFromDB) {
+ if (this._messageManager) {
+ let vanilla = LoginHelper.loginToVanillaObject(removedLogin);
+ this._messageManager.sendAsyncMessage("RemoteLogins:removeLogin",
+ { login: vanilla });
+ } else {
+ Services.logins.removeLogin(removedLogin);
+ }
+ }
+ }
+};
+
+/**
+ * A factory to generate FormLike objects that represent a set of login fields
+ * which aren't necessarily marked up with a <form> element.
+ */
+var LoginFormFactory = {
+ /**
+ * Create a LoginForm object from a <form>.
+ *
+ * @param {HTMLFormElement} aForm
+ * @return {LoginForm}
+ * @throws Error if aForm isn't an HTMLFormElement
+ */
+ createFromForm(aForm) {
+ let formLike = FormLikeFactory.createFromForm(aForm);
+ formLike.action = LoginUtils._getActionOrigin(aForm);
+
+ let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
+ state.loginFormRootElements.add(formLike.rootElement);
+ log("adding", formLike.rootElement, "to loginFormRootElements for", formLike.ownerDocument);
+
+ LoginManagerContent._formLikeByRootElement.set(formLike.rootElement, formLike);
+ return formLike;
+ },
+
+ /**
+ * Create a LoginForm object from a password or username field.
+ *
+ * If the field is in a <form>, construct the LoginForm from the form.
+ * Otherwise, create a LoginForm with a rootElement (wrapper) according to
+ * heuristics. Currently all <input> not in a <form> are one LoginForm but this
+ * shouldn't be relied upon as the heuristics may change to detect multiple
+ * "forms" (e.g. registration and login) on one page with a <form>.
+ *
+ * Note that two LoginForms created from the same field won't return the same LoginForm object.
+ * Use the `rootElement` property on the LoginForm as a key instead.
+ *
+ * @param {HTMLInputElement} aField - a password or username field in a document
+ * @return {LoginForm}
+ * @throws Error if aField isn't a password or username field in a document
+ */
+ createFromField(aField) {
+ if (!(aField instanceof Ci.nsIDOMHTMLInputElement) ||
+ (aField.type != "password" && !LoginHelper.isUsernameFieldType(aField)) ||
+ !aField.ownerDocument) {
+ throw new Error("createFromField requires a password or username field in a document");
+ }
+
+ if (aField.form) {
+ return this.createFromForm(aField.form);
+ }
+
+ let formLike = FormLikeFactory.createFromField(aField);
+ formLike.action = LoginUtils._getPasswordOrigin(aField.ownerDocument.baseURI);
+ log("Created non-form FormLike for rootElement:", aField.ownerDocument.documentElement);
+
+ let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
+ state.loginFormRootElements.add(formLike.rootElement);
+ log("adding", formLike.rootElement, "to loginFormRootElements for", formLike.ownerDocument);
+
+
+ LoginManagerContent._formLikeByRootElement.set(formLike.rootElement, formLike);
+
+ return formLike;
+ },
+};
diff --git a/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm b/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
new file mode 100644
index 000000000..5c88687bf
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["LoginManagerContextMenu"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
+ "resource://gre/modules/LoginManagerParent.jsm");
+
+/*
+ * Password manager object for the browser contextual menu.
+ */
+var LoginManagerContextMenu = {
+ /**
+ * Look for login items and add them to the contextual menu.
+ *
+ * @param {HTMLInputElement} inputElement
+ * The target input element of the context menu click.
+ * @param {xul:browser} browser
+ * The browser for the document the context menu was open on.
+ * @param {nsIURI} documentURI
+ * The URI of the document that the context menu was activated from.
+ * This isn't the same as the browser's top-level document URI
+ * when subframes are involved.
+ * @returns {DocumentFragment} a document fragment with all the login items.
+ */
+ addLoginsToMenu(inputElement, browser, documentURI) {
+ let foundLogins = this._findLogins(documentURI);
+
+ if (!foundLogins.length) {
+ return null;
+ }
+
+ let fragment = browser.ownerDocument.createDocumentFragment();
+ let duplicateUsernames = this._findDuplicates(foundLogins);
+ for (let login of foundLogins) {
+ let item = fragment.ownerDocument.createElement("menuitem");
+
+ let username = login.username;
+ // If login is empty or duplicated we want to append a modification date to it.
+ if (!username || duplicateUsernames.has(username)) {
+ if (!username) {
+ username = this._getLocalizedString("noUsername");
+ }
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ let time = this.dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+ username = this._getLocalizedString("loginHostAge", [username, time]);
+ }
+ item.setAttribute("label", username);
+ item.setAttribute("class", "context-login-item");
+
+ // login is bound so we can keep the reference to each object.
+ item.addEventListener("command", function(login, event) {
+ this._fillTargetField(login, inputElement, browser, documentURI);
+ }.bind(this, login));
+
+ fragment.appendChild(item);
+ }
+
+ return fragment;
+ },
+
+ /**
+ * Undoes the work of addLoginsToMenu for the same menu.
+ *
+ * @param {Document}
+ * The context menu owner document.
+ */
+ clearLoginsFromMenu(document) {
+ let loginItems = document.getElementsByClassName("context-login-item");
+ while (loginItems.item(0)) {
+ loginItems.item(0).remove();
+ }
+ },
+
+ /**
+ * Find logins for the current URI.
+ *
+ * @param {nsIURI} documentURI
+ * URI object with the hostname of the logins we want to find.
+ * This isn't the same as the browser's top-level document URI
+ * when subframes are involved.
+ *
+ * @returns {nsILoginInfo[]} a login list
+ */
+ _findLogins(documentURI) {
+ let searchParams = {
+ hostname: documentURI.prePath,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ };
+ let logins = LoginHelper.searchLoginsWithObject(searchParams);
+ let resolveBy = [
+ "scheme",
+ "timePasswordChanged",
+ ];
+ logins = LoginHelper.dedupeLogins(logins, ["username", "password"], resolveBy, documentURI.prePath);
+
+ // Sort logins in alphabetical order and by date.
+ logins.sort((loginA, loginB) => {
+ // Sort alphabetically
+ let result = loginA.username.localeCompare(loginB.username);
+ if (result) {
+ // Forces empty logins to be at the end
+ if (!loginA.username) {
+ return 1;
+ }
+ if (!loginB.username) {
+ return -1;
+ }
+ return result;
+ }
+
+ // Same username logins are sorted by last change date
+ let metaA = loginA.QueryInterface(Ci.nsILoginMetaInfo);
+ let metaB = loginB.QueryInterface(Ci.nsILoginMetaInfo);
+ return metaB.timePasswordChanged - metaA.timePasswordChanged;
+ });
+
+ return logins;
+ },
+
+ /**
+ * Find duplicate usernames in a login list.
+ *
+ * @param {nsILoginInfo[]} loginList
+ * A list of logins we want to look for duplicate usernames.
+ *
+ * @returns {Set} a set with the duplicate usernames.
+ */
+ _findDuplicates(loginList) {
+ let seen = new Set();
+ let duplicates = new Set();
+ for (let login of loginList) {
+ if (seen.has(login.username)) {
+ duplicates.add(login.username);
+ }
+ seen.add(login.username);
+ }
+ return duplicates;
+ },
+
+ /**
+ * @param {nsILoginInfo} login
+ * The login we want to fill the form with.
+ * @param {Element} inputElement
+ * The target input element we want to fill.
+ * @param {xul:browser} browser
+ * The target tab browser.
+ * @param {nsIURI} documentURI
+ * URI of the document owning the form we want to fill.
+ * This isn't the same as the browser's top-level
+ * document URI when subframes are involved.
+ */
+ _fillTargetField(login, inputElement, browser, documentURI) {
+ LoginManagerParent.fillForm({
+ browser: browser,
+ loginFormOrigin: documentURI.prePath,
+ login: login,
+ inputElement: inputElement,
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * @param {string} key
+ * The localized string key
+ * @param {string[]} formatArgs
+ * An array of formatting argument string
+ *
+ * @returns {string} the localized string for the specified key,
+ * formatted with arguments if required.
+ */
+ _getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return this._stringBundle.formatStringFromName(key, formatArgs, formatArgs.length);
+ }
+ return this._stringBundle.GetStringFromName(key);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(LoginManagerContextMenu, "_stringBundle", function() {
+ return Services.strings.
+ createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
+
+XPCOMUtils.defineLazyGetter(LoginManagerContextMenu, "dateAndTimeFormatter", function() {
+ return new Intl.DateTimeFormat(undefined, {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ });
+});
diff --git a/toolkit/components/passwordmgr/LoginManagerParent.jsm b/toolkit/components/passwordmgr/LoginManagerParent.jsm
new file mode 100644
index 000000000..e472fb61c
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -0,0 +1,511 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL"]);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UserAutoCompleteResult",
+ "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AutoCompletePopup",
+ "resource://gre/modules/AutoCompletePopup.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("LoginManagerParent");
+ return logger.log.bind(logger);
+});
+
+this.EXPORTED_SYMBOLS = [ "LoginManagerParent" ];
+
+var LoginManagerParent = {
+ /**
+ * Reference to the default LoginRecipesParent (instead of the initialization promise) for
+ * synchronous access. This is a temporary hack and new consumers should yield on
+ * recipeParentPromise instead.
+ *
+ * @type LoginRecipesParent
+ * @deprecated
+ */
+ _recipeManager: null,
+
+ // Tracks the last time the user cancelled the master password prompt,
+ // to avoid spamming master password prompts on autocomplete searches.
+ _lastMPLoginCancelled: Math.NEGATIVE_INFINITY,
+
+ _searchAndDedupeLogins: function (formOrigin, actionOrigin) {
+ let logins;
+ try {
+ logins = LoginHelper.searchLoginsWithObject({
+ hostname: formOrigin,
+ formSubmitURL: actionOrigin,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+ } catch (e) {
+ // Record the last time the user cancelled the MP prompt
+ // to avoid spamming them with MP prompts for autocomplete.
+ if (e.result == Cr.NS_ERROR_ABORT) {
+ log("User cancelled master password prompt.");
+ this._lastMPLoginCancelled = Date.now();
+ return [];
+ }
+ throw e;
+ }
+
+ // Dedupe so the length checks below still make sense with scheme upgrades.
+ let resolveBy = [
+ "scheme",
+ "timePasswordChanged",
+ ];
+ return LoginHelper.dedupeLogins(logins, ["username"], resolveBy, formOrigin);
+ },
+
+ init: function() {
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ mm.addMessageListener("RemoteLogins:findLogins", this);
+ mm.addMessageListener("RemoteLogins:findRecipes", this);
+ mm.addMessageListener("RemoteLogins:onFormSubmit", this);
+ mm.addMessageListener("RemoteLogins:autoCompleteLogins", this);
+ mm.addMessageListener("RemoteLogins:removeLogin", this);
+ mm.addMessageListener("RemoteLogins:insecureLoginFormPresent", this);
+
+ XPCOMUtils.defineLazyGetter(this, "recipeParentPromise", () => {
+ const { LoginRecipesParent } = Cu.import("resource://gre/modules/LoginRecipes.jsm", {});
+ this._recipeManager = new LoginRecipesParent({
+ defaults: Services.prefs.getComplexValue("signon.recipes.path", Ci.nsISupportsString).data,
+ });
+ return this._recipeManager.initializationPromise;
+ });
+ },
+
+ receiveMessage: function (msg) {
+ let data = msg.data;
+ switch (msg.name) {
+ case "RemoteLogins:findLogins": {
+ // TODO Verify msg.target's principals against the formOrigin?
+ this.sendLoginDataToChild(data.options.showMasterPassword,
+ data.formOrigin,
+ data.actionOrigin,
+ data.requestId,
+ msg.target.messageManager);
+ break;
+ }
+
+ case "RemoteLogins:findRecipes": {
+ let formHost = (new URL(data.formOrigin)).host;
+ return this._recipeManager.getRecipesForHost(formHost);
+ }
+
+ case "RemoteLogins:onFormSubmit": {
+ // TODO Verify msg.target's principals against the formOrigin?
+ this.onFormSubmit(data.hostname,
+ data.formSubmitURL,
+ data.usernameField,
+ data.newPasswordField,
+ data.oldPasswordField,
+ msg.objects.openerTopWindow,
+ msg.target);
+ break;
+ }
+
+ case "RemoteLogins:insecureLoginFormPresent": {
+ this.setHasInsecureLoginForms(msg.target, data.hasInsecureLoginForms);
+ break;
+ }
+
+ case "RemoteLogins:autoCompleteLogins": {
+ this.doAutocompleteSearch(data, msg.target);
+ break;
+ }
+
+ case "RemoteLogins:removeLogin": {
+ let login = LoginHelper.vanillaObjectToLogin(data.login);
+ AutoCompletePopup.removeLogin(login);
+ break;
+ }
+ }
+
+ return undefined;
+ },
+
+ /**
+ * Trigger a login form fill and send relevant data (e.g. logins and recipes)
+ * to the child process (LoginManagerContent).
+ */
+ fillForm: Task.async(function* ({ browser, loginFormOrigin, login, inputElement }) {
+ let recipes = [];
+ if (loginFormOrigin) {
+ let formHost;
+ try {
+ formHost = (new URL(loginFormOrigin)).host;
+ let recipeManager = yield this.recipeParentPromise;
+ recipes = recipeManager.getRecipesForHost(formHost);
+ } catch (ex) {
+ // Some schemes e.g. chrome aren't supported by URL
+ }
+ }
+
+ // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
+ // doesn't support structured cloning.
+ let jsLogins = [LoginHelper.loginToVanillaObject(login)];
+
+ let objects = inputElement ? {inputElement} : null;
+ browser.messageManager.sendAsyncMessage("RemoteLogins:fillForm", {
+ loginFormOrigin,
+ logins: jsLogins,
+ recipes,
+ }, objects);
+ }),
+
+ /**
+ * Send relevant data (e.g. logins and recipes) to the child process (LoginManagerContent).
+ */
+ sendLoginDataToChild: Task.async(function*(showMasterPassword, formOrigin, actionOrigin,
+ requestId, target) {
+ let recipes = [];
+ if (formOrigin) {
+ let formHost;
+ try {
+ formHost = (new URL(formOrigin)).host;
+ let recipeManager = yield this.recipeParentPromise;
+ recipes = recipeManager.getRecipesForHost(formHost);
+ } catch (ex) {
+ // Some schemes e.g. chrome aren't supported by URL
+ }
+ }
+
+ if (!showMasterPassword && !Services.logins.isLoggedIn) {
+ try {
+ target.sendAsyncMessage("RemoteLogins:loginsFound", {
+ requestId: requestId,
+ logins: [],
+ recipes,
+ });
+ } catch (e) {
+ log("error sending message to target", e);
+ }
+ return;
+ }
+
+ // If we're currently displaying a master password prompt, defer
+ // processing this form until the user handles the prompt.
+ if (Services.logins.uiBusy) {
+ log("deferring sendLoginDataToChild for", formOrigin);
+ let self = this;
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ observe: function (subject, topic, data) {
+ log("Got deferred sendLoginDataToChild notification:", topic);
+ // Only run observer once.
+ Services.obs.removeObserver(this, "passwordmgr-crypto-login");
+ Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
+ if (topic == "passwordmgr-crypto-loginCanceled") {
+ target.sendAsyncMessage("RemoteLogins:loginsFound", {
+ requestId: requestId,
+ logins: [],
+ recipes,
+ });
+ return;
+ }
+
+ self.sendLoginDataToChild(showMasterPassword, formOrigin, actionOrigin,
+ requestId, target);
+ },
+ };
+
+ // Possible leak: it's possible that neither of these notifications
+ // will fire, and if that happens, we'll leak the observer (and
+ // never return). We should guarantee that at least one of these
+ // will fire.
+ // See bug XXX.
+ Services.obs.addObserver(observer, "passwordmgr-crypto-login", false);
+ Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", false);
+ return;
+ }
+
+ let logins = this._searchAndDedupeLogins(formOrigin, actionOrigin);
+
+ log("sendLoginDataToChild:", logins.length, "deduped logins");
+ // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
+ // doesn't support structured cloning.
+ var jsLogins = LoginHelper.loginsToVanillaObjects(logins);
+ target.sendAsyncMessage("RemoteLogins:loginsFound", {
+ requestId: requestId,
+ logins: jsLogins,
+ recipes,
+ });
+ }),
+
+ doAutocompleteSearch: function({ formOrigin, actionOrigin,
+ searchString, previousResult,
+ rect, requestId, isSecure, isPasswordField,
+ remote }, target) {
+ // Note: previousResult is a regular object, not an
+ // nsIAutoCompleteResult.
+
+ // Cancel if we unsuccessfully prompted for the master password too recently.
+ if (!Services.logins.isLoggedIn) {
+ let timeDiff = Date.now() - this._lastMPLoginCancelled;
+ if (timeDiff < this._repromptTimeout) {
+ log("Not searching logins for autocomplete since the master password " +
+ `prompt was last cancelled ${Math.round(timeDiff / 1000)} seconds ago.`);
+ // Send an empty array to make LoginManagerContent clear the
+ // outstanding request it has temporarily saved.
+ target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
+ requestId,
+ logins: [],
+ });
+ return;
+ }
+ }
+
+ let searchStringLower = searchString.toLowerCase();
+ let logins;
+ if (previousResult &&
+ searchStringLower.startsWith(previousResult.searchString.toLowerCase())) {
+ log("Using previous autocomplete result");
+
+ // We have a list of results for a shorter search string, so just
+ // filter them further based on the new search string.
+ logins = LoginHelper.vanillaObjectsToLogins(previousResult.logins);
+ } else {
+ log("Creating new autocomplete search result.");
+
+ logins = this._searchAndDedupeLogins(formOrigin, actionOrigin);
+ }
+
+ let matchingLogins = logins.filter(function(fullMatch) {
+ let match = fullMatch.username;
+
+ // Remove results that are too short, or have different prefix.
+ // Also don't offer empty usernames as possible results except
+ // for password field.
+ if (isPasswordField) {
+ return true;
+ }
+ return match && match.toLowerCase().startsWith(searchStringLower);
+ });
+
+ // XXX In the E10S case, we're responsible for showing our own
+ // autocomplete popup here because the autocomplete protocol hasn't
+ // been e10s-ized yet. In the non-e10s case, our caller is responsible
+ // for showing the autocomplete popup (via the regular
+ // nsAutoCompleteController).
+ if (remote) {
+ let results = new UserAutoCompleteResult(searchString, matchingLogins, {isSecure});
+ AutoCompletePopup.showPopupWithResults({ browser: target.ownerDocument.defaultView, rect, results });
+ }
+
+ // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
+ // doesn't support structured cloning.
+ var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
+ target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
+ requestId: requestId,
+ logins: jsLogins,
+ });
+ },
+
+ onFormSubmit: function(hostname, formSubmitURL,
+ usernameField, newPasswordField,
+ oldPasswordField, openerTopWindow,
+ target) {
+ function getPrompter() {
+ var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
+ createInstance(Ci.nsILoginManagerPrompter);
+ prompterSvc.init(target.ownerDocument.defaultView);
+ prompterSvc.browser = target;
+ prompterSvc.opener = openerTopWindow;
+ return prompterSvc;
+ }
+
+ function recordLoginUse(login) {
+ // Update the lastUsed timestamp and increment the use count.
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ propBag.setProperty("timeLastUsed", Date.now());
+ propBag.setProperty("timesUsedIncrement", 1);
+ Services.logins.modifyLogin(login, propBag);
+ }
+
+ if (!Services.logins.getLoginSavingEnabled(hostname)) {
+ log("(form submission ignored -- saving is disabled for:", hostname, ")");
+ return;
+ }
+
+ var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ formLogin.init(hostname, formSubmitURL, null,
+ (usernameField ? usernameField.value : ""),
+ newPasswordField.value,
+ (usernameField ? usernameField.name : ""),
+ newPasswordField.name);
+
+ // Below here we have one login per hostPort + action + username with the
+ // matching scheme being preferred.
+ let logins = this._searchAndDedupeLogins(hostname, formSubmitURL);
+
+ // If we didn't find a username field, but seem to be changing a
+ // password, allow the user to select from a list of applicable
+ // logins to update the password for.
+ if (!usernameField && oldPasswordField && logins.length > 0) {
+ var prompter = getPrompter();
+
+ if (logins.length == 1) {
+ var oldLogin = logins[0];
+
+ if (oldLogin.password == formLogin.password) {
+ recordLoginUse(oldLogin);
+ log("(Not prompting to save/change since we have no username and the " +
+ "only saved password matches the new password)");
+ return;
+ }
+
+ formLogin.username = oldLogin.username;
+ formLogin.usernameField = oldLogin.usernameField;
+
+ prompter.promptToChangePassword(oldLogin, formLogin);
+ } else {
+ // Note: It's possible that that we already have the correct u+p saved
+ // but since we don't have the username, we don't know if the user is
+ // changing a second account to the new password so we ask anyways.
+
+ prompter.promptToChangePasswordWithUsernames(
+ logins, logins.length, formLogin);
+ }
+
+ return;
+ }
+
+
+ var existingLogin = null;
+ // Look for an existing login that matches the form login.
+ for (let login of logins) {
+ let same;
+
+ // If one login has a username but the other doesn't, ignore
+ // the username when comparing and only match if they have the
+ // same password. Otherwise, compare the logins and match even
+ // if the passwords differ.
+ if (!login.username && formLogin.username) {
+ var restoreMe = formLogin.username;
+ formLogin.username = "";
+ same = LoginHelper.doLoginsMatch(formLogin, login, {
+ ignorePassword: false,
+ ignoreSchemes: LoginHelper.schemeUpgrades,
+ });
+ formLogin.username = restoreMe;
+ } else if (!formLogin.username && login.username) {
+ formLogin.username = login.username;
+ same = LoginHelper.doLoginsMatch(formLogin, login, {
+ ignorePassword: false,
+ ignoreSchemes: LoginHelper.schemeUpgrades,
+ });
+ formLogin.username = ""; // we know it's always blank.
+ } else {
+ same = LoginHelper.doLoginsMatch(formLogin, login, {
+ ignorePassword: true,
+ ignoreSchemes: LoginHelper.schemeUpgrades,
+ });
+ }
+
+ if (same) {
+ existingLogin = login;
+ break;
+ }
+ }
+
+ if (existingLogin) {
+ log("Found an existing login matching this form submission");
+
+ // Change password if needed.
+ if (existingLogin.password != formLogin.password) {
+ log("...passwords differ, prompting to change.");
+ prompter = getPrompter();
+ prompter.promptToChangePassword(existingLogin, formLogin);
+ } else if (!existingLogin.username && formLogin.username) {
+ log("...empty username update, prompting to change.");
+ prompter = getPrompter();
+ prompter.promptToChangePassword(existingLogin, formLogin);
+ } else {
+ recordLoginUse(existingLogin);
+ }
+
+ return;
+ }
+
+
+ // Prompt user to save login (via dialog or notification bar)
+ prompter = getPrompter();
+ prompter.promptToSavePassword(formLogin);
+ },
+
+ /**
+ * Maps all the <browser> elements for tabs in the parent process to the
+ * current state used to display tab-specific UI.
+ *
+ * This mapping is not updated in case a web page is moved to a different
+ * chrome window by the swapDocShells method. In this case, it is possible
+ * that a UI update just requested for the login fill doorhanger and then
+ * delayed by a few hundred milliseconds will be lost. Later requests would
+ * use the new browser reference instead.
+ *
+ * Given that the case above is rare, and it would not cause any origin
+ * mismatch at the time of filling because the origin is checked later in the
+ * content process, this case is left unhandled.
+ */
+ loginFormStateByBrowser: new WeakMap(),
+
+ /**
+ * Retrieves a reference to the state object associated with the given
+ * browser. This is initialized to an empty object.
+ */
+ stateForBrowser(browser) {
+ let loginFormState = this.loginFormStateByBrowser.get(browser);
+ if (!loginFormState) {
+ loginFormState = {};
+ this.loginFormStateByBrowser.set(browser, loginFormState);
+ }
+ return loginFormState;
+ },
+
+ /**
+ * Returns true if the page currently loaded in the given browser element has
+ * insecure login forms. This state may be updated asynchronously, in which
+ * case a custom event named InsecureLoginFormsStateChange will be dispatched
+ * on the browser element.
+ */
+ hasInsecureLoginForms(browser) {
+ return !!this.stateForBrowser(browser).hasInsecureLoginForms;
+ },
+
+ /**
+ * Called to indicate whether an insecure password field is present so
+ * insecure password UI can know when to show.
+ */
+ setHasInsecureLoginForms(browser, hasInsecureLoginForms) {
+ let state = this.stateForBrowser(browser);
+
+ // Update the data to use to the latest known values. Since messages are
+ // processed in order, this will always be the latest version to use.
+ state.hasInsecureLoginForms = hasInsecureLoginForms;
+
+ // Report the insecure login form state immediately.
+ browser.dispatchEvent(new browser.ownerDocument.defaultView
+ .CustomEvent("InsecureLoginFormsStateChange"));
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(LoginManagerParent, "_repromptTimeout",
+ "signon.masterPasswordReprompt.timeout_ms", 900000); // 15 Minutes
diff --git a/toolkit/components/passwordmgr/LoginRecipes.jsm b/toolkit/components/passwordmgr/LoginRecipes.jsm
new file mode 100644
index 000000000..4a8124bbc
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginRecipes.jsm
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["LoginRecipesContent", "LoginRecipesParent"];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const REQUIRED_KEYS = ["hosts"];
+const OPTIONAL_KEYS = ["description", "notUsernameSelector", "passwordSelector", "pathRegex", "usernameSelector"];
+const SUPPORTED_KEYS = REQUIRED_KEYS.concat(OPTIONAL_KEYS);
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => LoginHelper.createLogger("LoginRecipes"));
+
+/**
+ * Create an instance of the object to manage recipes in the parent process.
+ * Consumers should wait until {@link initializationPromise} resolves before
+ * calling methods on the object.
+ *
+ * @constructor
+ * @param {String} [aOptions.defaults=null] the URI to load the recipes from.
+ * If it's null, nothing is loaded.
+ *
+*/
+function LoginRecipesParent(aOptions = { defaults: null }) {
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ throw new Error("LoginRecipesParent should only be used from the main process");
+ }
+ this._defaults = aOptions.defaults;
+ this.reset();
+}
+
+LoginRecipesParent.prototype = {
+ /**
+ * Promise resolved with an instance of itself when the module is ready.
+ *
+ * @type {Promise}
+ */
+ initializationPromise: null,
+
+ /**
+ * @type {bool} Whether default recipes were loaded at construction time.
+ */
+ _defaults: null,
+
+ /**
+ * @type {Map} Map of hosts (including non-default port numbers) to Sets of recipes.
+ * e.g. "example.com:8080" => Set({...})
+ */
+ _recipesByHost: null,
+
+ /**
+ * @param {Object} aRecipes an object containing recipes to load for use. The object
+ * should be compatible with JSON (e.g. no RegExp).
+ * @return {Promise} resolving when the recipes are loaded
+ */
+ load(aRecipes) {
+ let recipeErrors = 0;
+ for (let rawRecipe of aRecipes.siteRecipes) {
+ try {
+ rawRecipe.pathRegex = rawRecipe.pathRegex ? new RegExp(rawRecipe.pathRegex) : undefined;
+ this.add(rawRecipe);
+ } catch (ex) {
+ recipeErrors++;
+ log.error("Error loading recipe", rawRecipe, ex);
+ }
+ }
+
+ if (recipeErrors) {
+ return Promise.reject(`There were ${recipeErrors} recipe error(s)`);
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Reset the set of recipes to the ones from the time of construction.
+ */
+ reset() {
+ log.debug("Resetting recipes with defaults:", this._defaults);
+ this._recipesByHost = new Map();
+
+ if (this._defaults) {
+ let channel = NetUtil.newChannel({uri: NetUtil.newURI(this._defaults, "UTF-8"),
+ loadUsingSystemPrincipal: true});
+ channel.contentType = "application/json";
+
+ try {
+ this.initializationPromise = new Promise(function(resolve) {
+ NetUtil.asyncFetch(channel, function (stream, result) {
+ if (!Components.isSuccessCode(result)) {
+ throw new Error("Error fetching recipe file:" + result);
+ }
+ let count = stream.available();
+ let data = NetUtil.readInputStreamToString(stream, count, { charset: "UTF-8" });
+ resolve(JSON.parse(data));
+ });
+ }).then(recipes => {
+ return this.load(recipes);
+ }).then(resolve => {
+ return this;
+ });
+ } catch (e) {
+ throw new Error("Error reading recipe file:" + e);
+ }
+ } else {
+ this.initializationPromise = Promise.resolve(this);
+ }
+ },
+
+ /**
+ * Validate the recipe is sane and then add it to the set of recipes.
+ *
+ * @param {Object} recipe
+ */
+ add(recipe) {
+ log.debug("Adding recipe:", recipe);
+ let recipeKeys = Object.keys(recipe);
+ let unknownKeys = recipeKeys.filter(key => SUPPORTED_KEYS.indexOf(key) == -1);
+ if (unknownKeys.length > 0) {
+ throw new Error("The following recipe keys aren't supported: " + unknownKeys.join(", "));
+ }
+
+ let missingRequiredKeys = REQUIRED_KEYS.filter(key => recipeKeys.indexOf(key) == -1);
+ if (missingRequiredKeys.length > 0) {
+ throw new Error("The following required recipe keys are missing: " + missingRequiredKeys.join(", "));
+ }
+
+ if (!Array.isArray(recipe.hosts)) {
+ throw new Error("'hosts' must be a array");
+ }
+
+ if (!recipe.hosts.length) {
+ throw new Error("'hosts' must be a non-empty array");
+ }
+
+ if (recipe.pathRegex && recipe.pathRegex.constructor.name != "RegExp") {
+ throw new Error("'pathRegex' must be a regular expression");
+ }
+
+ const OPTIONAL_STRING_PROPS = ["description", "passwordSelector", "usernameSelector"];
+ for (let prop of OPTIONAL_STRING_PROPS) {
+ if (recipe[prop] && typeof(recipe[prop]) != "string") {
+ throw new Error(`'${prop}' must be a string`);
+ }
+ }
+
+ // Add the recipe to the map for each host
+ for (let host of recipe.hosts) {
+ if (!this._recipesByHost.has(host)) {
+ this._recipesByHost.set(host, new Set());
+ }
+ this._recipesByHost.get(host).add(recipe);
+ }
+ },
+
+ /**
+ * Currently only exact host matches are returned but this will eventually handle parent domains.
+ *
+ * @param {String} aHost (e.g. example.com:8080 [non-default port] or sub.example.com)
+ * @return {Set} of recipes that apply to the host ordered by host priority
+ */
+ getRecipesForHost(aHost) {
+ let hostRecipes = this._recipesByHost.get(aHost);
+ if (!hostRecipes) {
+ return new Set();
+ }
+
+ return hostRecipes;
+ },
+};
+
+
+var LoginRecipesContent = {
+ /**
+ * @param {Set} aRecipes - Possible recipes that could apply to the form
+ * @param {FormLike} aForm - We use a form instead of just a URL so we can later apply
+ * tests to the page contents.
+ * @return {Set} a subset of recipes that apply to the form with the order preserved
+ */
+ _filterRecipesForForm(aRecipes, aForm) {
+ let formDocURL = aForm.ownerDocument.location;
+ let hostRecipes = aRecipes;
+ let recipes = new Set();
+ log.debug("_filterRecipesForForm", aRecipes);
+ if (!hostRecipes) {
+ return recipes;
+ }
+
+ for (let hostRecipe of hostRecipes) {
+ if (hostRecipe.pathRegex && !hostRecipe.pathRegex.test(formDocURL.pathname)) {
+ continue;
+ }
+ recipes.add(hostRecipe);
+ }
+
+ return recipes;
+ },
+
+ /**
+ * Given a set of recipes that apply to the host, choose the one most applicable for
+ * overriding login fields in the form.
+ *
+ * @param {Set} aRecipes The set of recipes to consider for the form
+ * @param {FormLike} aForm The form where login fields exist.
+ * @return {Object} The recipe that is most applicable for the form.
+ */
+ getFieldOverrides(aRecipes, aForm) {
+ let recipes = this._filterRecipesForForm(aRecipes, aForm);
+ log.debug("getFieldOverrides: filtered recipes:", recipes);
+ if (!recipes.size) {
+ return null;
+ }
+
+ let chosenRecipe = null;
+ // Find the first (most-specific recipe that involves field overrides).
+ for (let recipe of recipes) {
+ if (!recipe.usernameSelector && !recipe.passwordSelector &&
+ !recipe.notUsernameSelector) {
+ continue;
+ }
+
+ chosenRecipe = recipe;
+ break;
+ }
+
+ return chosenRecipe;
+ },
+
+ /**
+ * @param {HTMLElement} aParent the element to query for the selector from.
+ * @param {CSSSelector} aSelector the CSS selector to query for the login field.
+ * @return {HTMLElement|null}
+ */
+ queryLoginField(aParent, aSelector) {
+ if (!aSelector) {
+ return null;
+ }
+ let field = aParent.ownerDocument.querySelector(aSelector);
+ if (!field) {
+ log.debug("Login field selector wasn't matched:", aSelector);
+ return null;
+ }
+ if (!(field instanceof aParent.ownerDocument.defaultView.HTMLInputElement)) {
+ log.warn("Login field isn't an <input> so ignoring it:", aSelector);
+ return null;
+ }
+ return field;
+ },
+};
diff --git a/toolkit/components/passwordmgr/LoginStore.jsm b/toolkit/components/passwordmgr/LoginStore.jsm
new file mode 100644
index 000000000..9fa6e7dff
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginStore.jsm
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Handles serialization of the data and persistence into a file.
+ *
+ * The file is stored in JSON format, without indentation, using UTF-8 encoding.
+ * With indentation applied, the file would look like this:
+ *
+ * {
+ * "logins": [
+ * {
+ * "id": 2,
+ * "hostname": "http://www.example.com",
+ * "httpRealm": null,
+ * "formSubmitURL": "http://www.example.com/submit-url",
+ * "usernameField": "username_field",
+ * "passwordField": "password_field",
+ * "encryptedUsername": "...",
+ * "encryptedPassword": "...",
+ * "guid": "...",
+ * "encType": 1,
+ * "timeCreated": 1262304000000,
+ * "timeLastUsed": 1262304000000,
+ * "timePasswordChanged": 1262476800000,
+ * "timesUsed": 1
+ * },
+ * {
+ * "id": 4,
+ * (...)
+ * }
+ * ],
+ * "disabledHosts": [
+ * "http://www.example.org",
+ * "http://www.example.net"
+ * ],
+ * "nextId": 10,
+ * "version": 1
+ * }
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginStore",
+];
+
+// Globals
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+ "resource://gre/modules/JSONFile.jsm");
+
+/**
+ * Current data version assigned by the code that last touched the data.
+ *
+ * This number should be updated only when it is important to understand whether
+ * an old version of the code has touched the data, for example to execute an
+ * update logic. In most cases, this number should not be changed, in
+ * particular when no special one-time update logic is needed.
+ *
+ * For example, this number should NOT be changed when a new optional field is
+ * added to a login entry.
+ */
+const kDataVersion = 2;
+
+// The permission type we store in the permission manager.
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+// LoginStore
+
+/**
+ * Inherits from JSONFile and handles serialization of login-related data and
+ * persistence into a file.
+ *
+ * @param aPath
+ * String containing the file path where data should be saved.
+ */
+function LoginStore(aPath) {
+ JSONFile.call(this, {
+ path: aPath,
+ dataPostProcessor: this._dataPostProcessor.bind(this)
+ });
+}
+
+LoginStore.prototype = Object.create(JSONFile.prototype);
+LoginStore.prototype.constructor = LoginStore;
+
+/**
+ * Synchronously work on the data just loaded into memory.
+ */
+LoginStore.prototype._dataPostProcessor = function(data) {
+ if (data.nextId === undefined) {
+ data.nextId = 1;
+ }
+
+ // Create any arrays that are not present in the saved file.
+ if (!data.logins) {
+ data.logins = [];
+ }
+
+ // Stub needed for login imports before data has been migrated.
+ if (!data.disabledHosts) {
+ data.disabledHosts = [];
+ }
+
+ if (data.version === 1) {
+ this._migrateDisabledHosts(data);
+ }
+
+ // Indicate that the current version of the code has touched the file.
+ data.version = kDataVersion;
+
+ return data;
+};
+
+/**
+ * Migrates disabled hosts to the permission manager.
+ */
+LoginStore.prototype._migrateDisabledHosts = function (data) {
+ for (let host of data.disabledHosts) {
+ try {
+ let uri = Services.io.newURI(host, null, null);
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ delete data.disabledHosts;
+};
diff --git a/toolkit/components/passwordmgr/OSCrypto.jsm b/toolkit/components/passwordmgr/OSCrypto.jsm
new file mode 100644
index 000000000..04254f66f
--- /dev/null
+++ b/toolkit/components/passwordmgr/OSCrypto.jsm
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Common front for various implementations of OSCrypto
+ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["OSCrypto"];
+
+var OSCrypto = {};
+
+if (AppConstants.platform == "win") {
+ Services.scriptloader.loadSubScript("resource://gre/modules/OSCrypto_win.js", this);
+} else {
+ throw new Error("OSCrypto.jsm isn't supported on this platform");
+}
diff --git a/toolkit/components/passwordmgr/OSCrypto_win.js b/toolkit/components/passwordmgr/OSCrypto_win.js
new file mode 100644
index 000000000..0f52f4269
--- /dev/null
+++ b/toolkit/components/passwordmgr/OSCrypto_win.js
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes", "resource://gre/modules/ctypes.jsm");
+
+const FLAGS_NOT_SET = 0;
+
+const wintypes = {
+ BOOL: ctypes.bool,
+ BYTE: ctypes.uint8_t,
+ DWORD: ctypes.uint32_t,
+ PBYTE: ctypes.unsigned_char.ptr,
+ PCHAR: ctypes.char.ptr,
+ PDWORD: ctypes.uint32_t.ptr,
+ PVOID: ctypes.voidptr_t,
+ WORD: ctypes.uint16_t,
+};
+
+function OSCrypto() {
+ this._structs = {};
+ this._functions = new Map();
+ this._libs = new Map();
+ this._structs.DATA_BLOB = new ctypes.StructType("DATA_BLOB",
+ [
+ {cbData: wintypes.DWORD},
+ {pbData: wintypes.PVOID}
+ ]);
+
+ try {
+
+ this._libs.set("crypt32", ctypes.open("Crypt32"));
+ this._libs.set("kernel32", ctypes.open("Kernel32"));
+
+ this._functions.set("CryptProtectData",
+ this._libs.get("crypt32").declare("CryptProtectData",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr));
+ this._functions.set("CryptUnprotectData",
+ this._libs.get("crypt32").declare("CryptUnprotectData",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr));
+ this._functions.set("LocalFree",
+ this._libs.get("kernel32").declare("LocalFree",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ wintypes.PVOID));
+ } catch (ex) {
+ Cu.reportError(ex);
+ this.finalize();
+ throw ex;
+ }
+}
+OSCrypto.prototype = {
+ /**
+ * Convert an array containing only two bytes unsigned numbers to a string.
+ * @param {number[]} arr - the array that needs to be converted.
+ * @returns {string} the string representation of the array.
+ */
+ arrayToString(arr) {
+ let str = "";
+ for (let i = 0; i < arr.length; i++) {
+ str += String.fromCharCode(arr[i]);
+ }
+ return str;
+ },
+
+ /**
+ * Convert a string to an array.
+ * @param {string} str - the string that needs to be converted.
+ * @returns {number[]} the array representation of the string.
+ */
+ stringToArray(str) {
+ let arr = [];
+ for (let i = 0; i < str.length; i++) {
+ arr.push(str.charCodeAt(i));
+ }
+ return arr;
+ },
+
+ /**
+ * Calculate the hash value used by IE as the name of the registry value where login details are
+ * stored.
+ * @param {string} data - the string value that needs to be hashed.
+ * @returns {string} the hash value of the string.
+ */
+ getIELoginHash(data) {
+ // return the two-digit hexadecimal code for a byte
+ function toHexString(charCode) {
+ return ("00" + charCode.toString(16)).slice(-2);
+ }
+
+ // the data needs to be encoded in null terminated UTF-16
+ data += "\0";
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-16";
+ // result is an out parameter,
+ // result.value will contain the array length
+ let result = {};
+ // dataArray is an array of bytes
+ let dataArray = converter.convertToByteArray(data, result);
+ // calculation of SHA1 hash value
+ let cryptoHash = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ cryptoHash.init(cryptoHash.SHA1);
+ cryptoHash.update(dataArray, dataArray.length);
+ let hash = cryptoHash.finish(false);
+
+ let tail = 0; // variable to calculate value for the last 2 bytes
+ // convert to a character string in hexadecimal notation
+ for (let c of hash) {
+ tail += c.charCodeAt(0);
+ }
+ hash += String.fromCharCode(tail % 256);
+
+ // convert the binary hash data to a hex string.
+ let hashStr = Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
+ return hashStr.toUpperCase();
+ },
+
+ /**
+ * Decrypt a string using the windows CryptUnprotectData API.
+ * @param {string} data - the encrypted string that needs to be decrypted.
+ * @param {?string} entropy - the entropy value of the decryption (could be null). Its value must
+ * be the same as the one used when the data was encrypted.
+ * @returns {string} the decryption of the string.
+ */
+ decryptData(data, entropy = null) {
+ let array = this.stringToArray(data);
+ let decryptedData = "";
+ let encryptedData = wintypes.BYTE.array(array.length)(array);
+ let inData = new this._structs.DATA_BLOB(encryptedData.length, encryptedData);
+ let outData = new this._structs.DATA_BLOB();
+ let entropyParam;
+ if (entropy) {
+ let entropyArray = this.stringToArray(entropy);
+ entropyArray.push(0);
+ let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
+ let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
+ entropyData);
+ entropyParam = optionalEntropy.address();
+ } else {
+ entropyParam = null;
+ }
+
+ let status = this._functions.get("CryptUnprotectData")(inData.address(), null,
+ entropyParam,
+ null, null, FLAGS_NOT_SET,
+ outData.address());
+ if (status === 0) {
+ throw new Error("decryptData failed: " + status);
+ }
+
+ // convert byte array to JS string.
+ let len = outData.cbData;
+ let decrypted = ctypes.cast(outData.pbData,
+ wintypes.BYTE.array(len).ptr).contents;
+ for (let i = 0; i < decrypted.length; i++) {
+ decryptedData += String.fromCharCode(decrypted[i]);
+ }
+
+ this._functions.get("LocalFree")(outData.pbData);
+ return decryptedData;
+ },
+
+ /**
+ * Encrypt a string using the windows CryptProtectData API.
+ * @param {string} data - the string that is going to be encrypted.
+ * @param {?string} entropy - the entropy value of the encryption (could be null). Its value must
+ * be the same as the one that is going to be used for the decryption.
+ * @returns {string} the encrypted string.
+ */
+ encryptData(data, entropy = null) {
+ let encryptedData = "";
+ let decryptedData = wintypes.BYTE.array(data.length)(this.stringToArray(data));
+
+ let inData = new this._structs.DATA_BLOB(data.length, decryptedData);
+ let outData = new this._structs.DATA_BLOB();
+ let entropyParam;
+ if (!entropy) {
+ entropyParam = null;
+ } else {
+ let entropyArray = this.stringToArray(entropy);
+ entropyArray.push(0);
+ let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
+ let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
+ entropyData);
+ entropyParam = optionalEntropy.address();
+ }
+
+ let status = this._functions.get("CryptProtectData")(inData.address(), null,
+ entropyParam,
+ null, null, FLAGS_NOT_SET,
+ outData.address());
+ if (status === 0) {
+ throw new Error("encryptData failed: " + status);
+ }
+
+ // convert byte array to JS string.
+ let len = outData.cbData;
+ let encrypted = ctypes.cast(outData.pbData,
+ wintypes.BYTE.array(len).ptr).contents;
+ encryptedData = this.arrayToString(encrypted);
+ this._functions.get("LocalFree")(outData.pbData);
+ return encryptedData;
+ },
+
+ /**
+ * Must be invoked once after last use of any of the provided helpers.
+ */
+ finalize() {
+ this._structs = {};
+ this._functions.clear();
+ for (let lib of this._libs.values()) {
+ try {
+ lib.close();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ this._libs.clear();
+ },
+};
diff --git a/toolkit/components/passwordmgr/content/passwordManager.js b/toolkit/components/passwordmgr/content/passwordManager.js
new file mode 100644
index 000000000..333dc1d24
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/passwordManager.js
@@ -0,0 +1,728 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** * =================== SAVED SIGNONS CODE =================== ***/
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+let kSignonBundle;
+
+// Default value for signon table sorting
+let lastSignonSortColumn = "hostname";
+let lastSignonSortAscending = true;
+
+let showingPasswords = false;
+
+// password-manager lists
+let signons = [];
+let deletedSignons = [];
+
+// Elements that would be used frequently
+let filterField;
+let togglePasswordsButton;
+let signonsIntro;
+let removeButton;
+let removeAllButton;
+let signonsTree;
+
+let signonReloadDisplay = {
+ observe: function(subject, topic, data) {
+ if (topic == "passwordmgr-storage-changed") {
+ switch (data) {
+ case "addLogin":
+ case "modifyLogin":
+ case "removeLogin":
+ case "removeAllLogins":
+ if (!signonsTree) {
+ return;
+ }
+ signons.length = 0;
+ LoadSignons();
+ // apply the filter if needed
+ if (filterField && filterField.value != "") {
+ FilterPasswords();
+ }
+ break;
+ }
+ Services.obs.notifyObservers(null, "passwordmgr-dialog-updated", null);
+ }
+ }
+};
+
+// Formatter for localization.
+let dateFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric",
+ hour: "numeric", minute: "numeric" });
+
+function Startup() {
+ // be prepared to reload the display if anything changes
+ Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed", false);
+
+ signonsTree = document.getElementById("signonsTree");
+ kSignonBundle = document.getElementById("signonBundle");
+ filterField = document.getElementById("filter");
+ togglePasswordsButton = document.getElementById("togglePasswords");
+ signonsIntro = document.getElementById("signonsIntro");
+ removeButton = document.getElementById("removeSignon");
+ removeAllButton = document.getElementById("removeAllSignons");
+
+ togglePasswordsButton.label = kSignonBundle.getString("showPasswords");
+ togglePasswordsButton.accessKey = kSignonBundle.getString("showPasswordsAccessKey");
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionAll");
+ document.getElementsByTagName("treecols")[0].addEventListener("click", (event) => {
+ let { target, button } = event;
+ let sortField = target.getAttribute("data-field-name");
+
+ if (target.nodeName != "treecol" || button != 0 || !sortField) {
+ return;
+ }
+
+ SignonColumnSort(sortField);
+ Services.telemetry.getKeyedHistogramById("PWMGR_MANAGE_SORTED").add(sortField);
+ });
+
+ LoadSignons();
+
+ // filter the table if requested by caller
+ if (window.arguments &&
+ window.arguments[0] &&
+ window.arguments[0].filterString) {
+ setFilter(window.arguments[0].filterString);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_OPENED").add(1);
+ } else {
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_OPENED").add(0);
+ }
+
+ FocusFilterBox();
+}
+
+function Shutdown() {
+ Services.obs.removeObserver(signonReloadDisplay, "passwordmgr-storage-changed");
+}
+
+function setFilter(aFilterString) {
+ filterField.value = aFilterString;
+ FilterPasswords();
+}
+
+let signonsTreeView = {
+ // Keep track of which favicons we've fetched or started fetching.
+ // Maps a login origin to a favicon URL.
+ _faviconMap: new Map(),
+ _filterSet: [],
+ // Coalesce invalidations to avoid repeated flickering.
+ _invalidateTask: new DeferredTask(() => {
+ signonsTree.treeBoxObject.invalidateColumn(signonsTree.columns.siteCol);
+ }, 10),
+ _lastSelectedRanges: [],
+ selection: null,
+
+ rowCount: 0,
+ setTree(tree) {},
+ getImageSrc(row, column) {
+ if (column.element.getAttribute("id") !== "siteCol") {
+ return "";
+ }
+
+ const signon = this._filterSet.length ? this._filterSet[row] : signons[row];
+
+ // We already have the favicon URL or we started to fetch (value is null).
+ if (this._faviconMap.has(signon.hostname)) {
+ return this._faviconMap.get(signon.hostname);
+ }
+
+ // Record the fact that we already starting fetching a favicon for this
+ // origin in order to avoid multiple requests for the same origin.
+ this._faviconMap.set(signon.hostname, null);
+
+ PlacesUtils.promiseFaviconLinkUrl(signon.hostname)
+ .then(faviconURI => {
+ this._faviconMap.set(signon.hostname, faviconURI.spec);
+ this._invalidateTask.arm();
+ }).catch(Cu.reportError);
+
+ return "";
+ },
+ getProgressMode(row, column) {},
+ getCellValue(row, column) {},
+ getCellText(row, column) {
+ let time;
+ let signon = this._filterSet.length ? this._filterSet[row] : signons[row];
+ switch (column.id) {
+ case "siteCol":
+ return signon.httpRealm ?
+ (signon.hostname + " (" + signon.httpRealm + ")") :
+ signon.hostname;
+ case "userCol":
+ return signon.username || "";
+ case "passwordCol":
+ return signon.password || "";
+ case "timeCreatedCol":
+ time = new Date(signon.timeCreated);
+ return dateFormatter.format(time);
+ case "timeLastUsedCol":
+ time = new Date(signon.timeLastUsed);
+ return dateAndTimeFormatter.format(time);
+ case "timePasswordChangedCol":
+ time = new Date(signon.timePasswordChanged);
+ return dateFormatter.format(time);
+ case "timesUsedCol":
+ return signon.timesUsed;
+ default:
+ return "";
+ }
+ },
+ isEditable(row, col) {
+ if (col.id == "userCol" || col.id == "passwordCol") {
+ return true;
+ }
+ return false;
+ },
+ isSeparator(index) { return false; },
+ isSorted() { return false; },
+ isContainer(index) { return false; },
+ cycleHeader(column) {},
+ getRowProperties(row) { return ""; },
+ getColumnProperties(column) { return ""; },
+ getCellProperties(row, column) {
+ if (column.element.getAttribute("id") == "siteCol")
+ return "ltr";
+
+ return "";
+ },
+ setCellText(row, col, value) {
+ // If there is a filter, _filterSet needs to be used, otherwise signons is used.
+ let table = signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons;
+ function _editLogin(field) {
+ if (value == table[row][field]) {
+ return;
+ }
+ let existingLogin = table[row].clone();
+ table[row][field] = value;
+ table[row].timePasswordChanged = Date.now();
+ Services.logins.modifyLogin(existingLogin, table[row]);
+ signonsTree.treeBoxObject.invalidateRow(row);
+ }
+
+ if (col.id == "userCol") {
+ _editLogin("username");
+
+ } else if (col.id == "passwordCol") {
+ if (!value) {
+ return;
+ }
+ _editLogin("password");
+ }
+ },
+};
+
+function SortTree(column, ascending) {
+ let table = signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons;
+ // remember which item was selected so we can restore it after the sort
+ let selections = GetTreeSelections();
+ let selectedNumber = selections.length ? table[selections[0]].number : -1;
+
+ function compareFunc(a, b) {
+ let valA, valB;
+ switch (column) {
+ case "hostname":
+ let realmA = a.httpRealm;
+ let realmB = b.httpRealm;
+ realmA = realmA == null ? "" : realmA.toLowerCase();
+ realmB = realmB == null ? "" : realmB.toLowerCase();
+
+ valA = a[column].toLowerCase() + realmA;
+ valB = b[column].toLowerCase() + realmB;
+ break;
+ case "username":
+ case "password":
+ valA = a[column].toLowerCase();
+ valB = b[column].toLowerCase();
+ break;
+
+ default:
+ valA = a[column];
+ valB = b[column];
+ }
+
+ if (valA < valB)
+ return -1;
+ if (valA > valB)
+ return 1;
+ return 0;
+ }
+
+ // do the sort
+ table.sort(compareFunc);
+ if (!ascending) {
+ table.reverse();
+ }
+
+ // restore the selection
+ let selectedRow = -1;
+ if (selectedNumber >= 0 && false) {
+ for (let s = 0; s < table.length; s++) {
+ if (table[s].number == selectedNumber) {
+ // update selection
+ // note: we need to deselect before reselecting in order to trigger ...Selected()
+ signonsTree.view.selection.select(-1);
+ signonsTree.view.selection.select(s);
+ selectedRow = s;
+ break;
+ }
+ }
+ }
+
+ // display the results
+ signonsTree.treeBoxObject.invalidate();
+ if (selectedRow >= 0) {
+ signonsTree.treeBoxObject.ensureRowIsVisible(selectedRow);
+ }
+}
+
+function LoadSignons() {
+ // loads signons into table
+ try {
+ signons = Services.logins.getAllLogins();
+ } catch (e) {
+ signons = [];
+ }
+ signons.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo));
+ signonsTreeView.rowCount = signons.length;
+
+ // sort and display the table
+ signonsTree.view = signonsTreeView;
+ // The sort column didn't change. SortTree (called by
+ // SignonColumnSort) assumes we want to toggle the sort
+ // direction but here we don't so we have to trick it
+ lastSignonSortAscending = !lastSignonSortAscending;
+ SignonColumnSort(lastSignonSortColumn);
+
+ // disable "remove all signons" button if there are no signons
+ if (signons.length == 0) {
+ removeAllButton.setAttribute("disabled", "true");
+ togglePasswordsButton.setAttribute("disabled", "true");
+ } else {
+ removeAllButton.removeAttribute("disabled");
+ togglePasswordsButton.removeAttribute("disabled");
+ }
+
+ return true;
+}
+
+function GetTreeSelections() {
+ let selections = [];
+ let select = signonsTree.view.selection;
+ if (select) {
+ let count = select.getRangeCount();
+ let min = {};
+ let max = {};
+ for (let i = 0; i < count; i++) {
+ select.getRangeAt(i, min, max);
+ for (let k = min.value; k <= max.value; k++) {
+ if (k != -1) {
+ selections[selections.length] = k;
+ }
+ }
+ }
+ }
+ return selections;
+}
+
+function SignonSelected() {
+ let selections = GetTreeSelections();
+ if (selections.length) {
+ removeButton.removeAttribute("disabled");
+ } else {
+ removeButton.setAttribute("disabled", true);
+ }
+}
+
+function DeleteSignon() {
+ let filterSet = signonsTreeView._filterSet;
+ let syncNeeded = (filterSet.length != 0);
+ let tree = signonsTree;
+ let view = signonsTreeView;
+ let table = filterSet.length ? filterSet : signons;
+
+ // Turn off tree selection notifications during the deletion
+ tree.view.selection.selectEventsSuppressed = true;
+
+ // remove selected items from list (by setting them to null) and place in deleted list
+ let selections = GetTreeSelections();
+ for (let s = selections.length - 1; s >= 0; s--) {
+ let i = selections[s];
+ deletedSignons.push(table[i]);
+ table[i] = null;
+ }
+
+ // collapse list by removing all the null entries
+ for (let j = 0; j < table.length; j++) {
+ if (table[j] == null) {
+ let k = j;
+ while ((k < table.length) && (table[k] == null)) {
+ k++;
+ }
+ table.splice(j, k - j);
+ view.rowCount -= k - j;
+ tree.treeBoxObject.rowCountChanged(j, j - k);
+ }
+ }
+
+ // update selection and/or buttons
+ if (table.length) {
+ // update selection
+ let nextSelection = (selections[0] < table.length) ? selections[0] : table.length - 1;
+ tree.view.selection.select(nextSelection);
+ tree.treeBoxObject.ensureRowIsVisible(nextSelection);
+ } else {
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ }
+ tree.view.selection.selectEventsSuppressed = false;
+ FinalizeSignonDeletions(syncNeeded);
+}
+
+function DeleteAllSignons() {
+ let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Ci.nsIPromptService);
+
+ // Confirm the user wants to remove all passwords
+ let dummy = { value: false };
+ if (prompter.confirmEx(window,
+ kSignonBundle.getString("removeAllPasswordsTitle"),
+ kSignonBundle.getString("removeAllPasswordsPrompt"),
+ prompter.STD_YES_NO_BUTTONS + prompter.BUTTON_POS_1_DEFAULT,
+ null, null, null, null, dummy) == 1) // 1 == "No" button
+ return;
+
+ let filterSet = signonsTreeView._filterSet;
+ let syncNeeded = (filterSet.length != 0);
+ let view = signonsTreeView;
+ let table = filterSet.length ? filterSet : signons;
+
+ // remove all items from table and place in deleted table
+ for (let i = 0; i < table.length; i++) {
+ deletedSignons.push(table[i]);
+ }
+ table.length = 0;
+
+ // clear out selections
+ view.selection.select(-1);
+
+ // update the tree view and notify the tree
+ view.rowCount = 0;
+
+ let box = signonsTree.treeBoxObject;
+ box.rowCountChanged(0, -deletedSignons.length);
+ box.invalidate();
+
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ FinalizeSignonDeletions(syncNeeded);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED_ALL").add(1);
+}
+
+function TogglePasswordVisible() {
+ if (showingPasswords || masterPasswordLogin(AskUserShowPasswords)) {
+ showingPasswords = !showingPasswords;
+ togglePasswordsButton.label = kSignonBundle.getString(showingPasswords ? "hidePasswords" : "showPasswords");
+ togglePasswordsButton.accessKey = kSignonBundle.getString(showingPasswords ? "hidePasswordsAccessKey" : "showPasswordsAccessKey");
+ document.getElementById("passwordCol").hidden = !showingPasswords;
+ FilterPasswords();
+ }
+
+ // Notify observers that the password visibility toggling is
+ // completed. (Mostly useful for tests)
+ Services.obs.notifyObservers(null, "passwordmgr-password-toggle-complete", null);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_VISIBILITY_TOGGLED").add(showingPasswords);
+}
+
+function AskUserShowPasswords() {
+ let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
+ let dummy = { value: false };
+
+ // Confirm the user wants to display passwords
+ return prompter.confirmEx(window,
+ null,
+ kSignonBundle.getString("noMasterPasswordPrompt"), prompter.STD_YES_NO_BUTTONS,
+ null, null, null, null, dummy) == 0; // 0=="Yes" button
+}
+
+function FinalizeSignonDeletions(syncNeeded) {
+ for (let s = 0; s < deletedSignons.length; s++) {
+ Services.logins.removeLogin(deletedSignons[s]);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED").add(1);
+ }
+ // If the deletion has been performed in a filtered view, reflect the deletion in the unfiltered table.
+ // See bug 405389.
+ if (syncNeeded) {
+ try {
+ signons = Services.logins.getAllLogins();
+ } catch (e) {
+ signons = [];
+ }
+ }
+ deletedSignons.length = 0;
+}
+
+function HandleSignonKeyPress(e) {
+ // If editing is currently performed, don't do anything.
+ if (signonsTree.getAttribute("editing")) {
+ return;
+ }
+ if (e.keyCode == KeyboardEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyboardEvent.DOM_VK_BACK_SPACE)) {
+ DeleteSignon();
+ }
+}
+
+function getColumnByName(column) {
+ switch (column) {
+ case "hostname":
+ return document.getElementById("siteCol");
+ case "username":
+ return document.getElementById("userCol");
+ case "password":
+ return document.getElementById("passwordCol");
+ case "timeCreated":
+ return document.getElementById("timeCreatedCol");
+ case "timeLastUsed":
+ return document.getElementById("timeLastUsedCol");
+ case "timePasswordChanged":
+ return document.getElementById("timePasswordChangedCol");
+ case "timesUsed":
+ return document.getElementById("timesUsedCol");
+ }
+ return undefined;
+}
+
+function SignonColumnSort(column) {
+ let sortedCol = getColumnByName(column);
+ let lastSortedCol = getColumnByName(lastSignonSortColumn);
+
+ // clear out the sortDirection attribute on the old column
+ lastSortedCol.removeAttribute("sortDirection");
+
+ // determine if sort is to be ascending or descending
+ lastSignonSortAscending = (column == lastSignonSortColumn) ? !lastSignonSortAscending : true;
+
+ // sort
+ lastSignonSortColumn = column;
+ SortTree(lastSignonSortColumn, lastSignonSortAscending);
+
+ // set the sortDirection attribute to get the styling going
+ // first we need to get the right element
+ sortedCol.setAttribute("sortDirection", lastSignonSortAscending ?
+ "ascending" : "descending");
+}
+
+function SignonClearFilter() {
+ let singleSelection = (signonsTreeView.selection.count == 1);
+
+ // Clear the Tree Display
+ signonsTreeView.rowCount = 0;
+ signonsTree.treeBoxObject.rowCountChanged(0, -signonsTreeView._filterSet.length);
+ signonsTreeView._filterSet = [];
+
+ // Just reload the list to make sure deletions are respected
+ LoadSignons();
+
+ // Restore selection
+ if (singleSelection) {
+ signonsTreeView.selection.clearSelection();
+ for (let i = 0; i < signonsTreeView._lastSelectedRanges.length; ++i) {
+ let range = signonsTreeView._lastSelectedRanges[i];
+ signonsTreeView.selection.rangedSelect(range.min, range.max, true);
+ }
+ } else {
+ signonsTreeView.selection.select(0);
+ }
+ signonsTreeView._lastSelectedRanges = [];
+
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionAll");
+}
+
+function FocusFilterBox() {
+ if (filterField.getAttribute("focused") != "true") {
+ filterField.focus();
+ }
+}
+
+function SignonMatchesFilter(aSignon, aFilterValue) {
+ if (aSignon.hostname.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (aSignon.username &&
+ aSignon.username.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (aSignon.httpRealm &&
+ aSignon.httpRealm.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (showingPasswords && aSignon.password &&
+ aSignon.password.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+
+ return false;
+}
+
+function _filterPasswords(aFilterValue, view) {
+ aFilterValue = aFilterValue.toLowerCase();
+ return signons.filter(s => SignonMatchesFilter(s, aFilterValue));
+}
+
+function SignonSaveState() {
+ // Save selection
+ let seln = signonsTreeView.selection;
+ signonsTreeView._lastSelectedRanges = [];
+ let rangeCount = seln.getRangeCount();
+ for (let i = 0; i < rangeCount; ++i) {
+ let min = {}; let max = {};
+ seln.getRangeAt(i, min, max);
+ signonsTreeView._lastSelectedRanges.push({ min: min.value, max: max.value });
+ }
+}
+
+function FilterPasswords() {
+ if (filterField.value == "") {
+ SignonClearFilter();
+ return;
+ }
+
+ let newFilterSet = _filterPasswords(filterField.value, signonsTreeView);
+ if (!signonsTreeView._filterSet.length) {
+ // Save Display Info for the Non-Filtered mode when we first
+ // enter Filtered mode.
+ SignonSaveState();
+ }
+ signonsTreeView._filterSet = newFilterSet;
+
+ // Clear the display
+ let oldRowCount = signonsTreeView.rowCount;
+ signonsTreeView.rowCount = 0;
+ signonsTree.treeBoxObject.rowCountChanged(0, -oldRowCount);
+ // Set up the filtered display
+ signonsTreeView.rowCount = signonsTreeView._filterSet.length;
+ signonsTree.treeBoxObject.rowCountChanged(0, signonsTreeView.rowCount);
+
+ // if the view is not empty then select the first item
+ if (signonsTreeView.rowCount > 0)
+ signonsTreeView.selection.select(0);
+
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionFiltered");
+}
+
+function CopyPassword() {
+ // Don't copy passwords if we aren't already showing the passwords & a master
+ // password hasn't been entered.
+ if (!showingPasswords && !masterPasswordLogin())
+ return;
+ // Copy selected signon's password to clipboard
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ let row = signonsTree.currentIndex;
+ let password = signonsTreeView.getCellText(row, {id : "passwordCol" });
+ clipboard.copyString(password);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_PASSWORD").add(1);
+}
+
+function CopyUsername() {
+ // Copy selected signon's username to clipboard
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ let row = signonsTree.currentIndex;
+ let username = signonsTreeView.getCellText(row, {id : "userCol" });
+ clipboard.copyString(username);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_USERNAME").add(1);
+}
+
+function EditCellInSelectedRow(columnName) {
+ let row = signonsTree.currentIndex;
+ let columnElement = getColumnByName(columnName);
+ signonsTree.startEditing(row, signonsTree.columns.getColumnFor(columnElement));
+}
+
+function UpdateContextMenu() {
+ let singleSelection = (signonsTreeView.selection.count == 1);
+ let menuItems = new Map();
+ let menupopup = document.getElementById("signonsTreeContextMenu");
+ for (let menuItem of menupopup.querySelectorAll("menuitem")) {
+ menuItems.set(menuItem.id, menuItem);
+ }
+
+ if (!singleSelection) {
+ for (let menuItem of menuItems.values()) {
+ menuItem.setAttribute("disabled", "true");
+ }
+ return;
+ }
+
+ let selectedRow = signonsTree.currentIndex;
+
+ // Disable "Copy Username" if the username is empty.
+ if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") {
+ menuItems.get("context-copyusername").removeAttribute("disabled");
+ } else {
+ menuItems.get("context-copyusername").setAttribute("disabled", "true");
+ }
+
+ menuItems.get("context-editusername").removeAttribute("disabled");
+ menuItems.get("context-copypassword").removeAttribute("disabled");
+
+ // Disable "Edit Password" if the password column isn't showing.
+ if (!document.getElementById("passwordCol").hidden) {
+ menuItems.get("context-editpassword").removeAttribute("disabled");
+ } else {
+ menuItems.get("context-editpassword").setAttribute("disabled", "true");
+ }
+}
+
+function masterPasswordLogin(noPasswordCallback) {
+ // This doesn't harm if passwords are not encrypted
+ let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .createInstance(Ci.nsIPK11TokenDB);
+ let token = tokendb.getInternalKeyToken();
+
+ // If there is no master password, still give the user a chance to opt-out of displaying passwords
+ if (token.checkPassword(""))
+ return noPasswordCallback ? noPasswordCallback() : true;
+
+ // So there's a master password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl).
+ try {
+ // Relogin and ask for the master password.
+ token.login(true); // 'true' means always prompt for token password. User will be prompted until
+ // clicking 'Cancel' or entering the correct password.
+ } catch (e) {
+ // An exception will be thrown if the user cancels the login prompt dialog.
+ // User is also logged out of Software Security Device.
+ }
+
+ return token.isLoggedIn();
+}
+
+function escapeKeyHandler() {
+ // If editing is currently performed, don't do anything.
+ if (signonsTree.getAttribute("editing")) {
+ return;
+ }
+ window.close();
+}
+
+function OpenMigrator() {
+ const { MigrationUtils } = Cu.import("resource:///modules/MigrationUtils.jsm", {});
+ // We pass in the type of source we're using for use in telemetry:
+ MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_PASSWORDS]);
+}
diff --git a/toolkit/components/passwordmgr/content/passwordManager.xul b/toolkit/components/passwordmgr/content/passwordManager.xul
new file mode 100644
index 000000000..d248283b6
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/passwordManager.xul
@@ -0,0 +1,134 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil -*- -->
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/passwordmgr.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://passwordmgr/locale/passwordManager.dtd" >
+
+<window id="SignonViewerDialog"
+ windowtype="Toolkit:PasswordManager"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="Startup();"
+ onunload="Shutdown();"
+ title="&savedLogins.title;"
+ style="width: 45em;"
+ persist="width height screenX screenY">
+
+ <script type="application/javascript" src="chrome://passwordmgr/content/passwordManager.js"/>
+
+ <stringbundle id="signonBundle"
+ src="chrome://passwordmgr/locale/passwordmgr.properties"/>
+
+ <keyset>
+ <key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();"/>
+ <key key="&windowClose.key;" modifiers="accel" oncommand="escapeKeyHandler();"/>
+ <key key="&focusSearch1.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
+ <key key="&focusSearch2.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
+ </keyset>
+
+ <popupset id="signonsTreeContextSet">
+ <menupopup id="signonsTreeContextMenu"
+ onpopupshowing="UpdateContextMenu()">
+ <menuitem id="context-copyusername"
+ label="&copyUsernameCmd.label;"
+ accesskey="&copyUsernameCmd.accesskey;"
+ oncommand="CopyUsername()"/>
+ <menuitem id="context-editusername"
+ label="&editUsernameCmd.label;"
+ accesskey="&editUsernameCmd.accesskey;"
+ oncommand="EditCellInSelectedRow('username')"/>
+ <menuseparator/>
+ <menuitem id="context-copypassword"
+ label="&copyPasswordCmd.label;"
+ accesskey="&copyPasswordCmd.accesskey;"
+ oncommand="CopyPassword()"/>
+ <menuitem id="context-editpassword"
+ label="&editPasswordCmd.label;"
+ accesskey="&editPasswordCmd.accesskey;"
+ oncommand="EditCellInSelectedRow('password')"/>
+ </menupopup>
+ </popupset>
+
+ <!-- saved signons -->
+ <vbox id="savedsignons" class="contentPane" flex="1">
+ <!-- filter -->
+ <hbox align="center">
+ <label accesskey="&filter.accesskey;" control="filter">&filter.label;</label>
+ <textbox id="filter" flex="1" type="search"
+ aria-controls="signonsTree"
+ oncommand="FilterPasswords();"/>
+ </hbox>
+
+ <label control="signonsTree" id="signonsIntro"/>
+ <separator class="thin"/>
+ <tree id="signonsTree" flex="1"
+ width="750"
+ style="height: 20em;"
+ onkeypress="HandleSignonKeyPress(event)"
+ onselect="SignonSelected();"
+ editable="true"
+ context="signonsTreeContextMenu">
+ <treecols>
+ <treecol id="siteCol" label="&treehead.site.label;" flex="40"
+ data-field-name="hostname" persist="width"
+ ignoreincolumnpicker="true"
+ sortDirection="ascending"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="userCol" label="&treehead.username.label;" flex="25"
+ ignoreincolumnpicker="true"
+ data-field-name="username" persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="passwordCol" label="&treehead.password.label;" flex="15"
+ ignoreincolumnpicker="true"
+ data-field-name="password" persist="width"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timeCreatedCol" label="&treehead.timeCreated.label;" flex="10"
+ data-field-name="timeCreated" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timeLastUsedCol" label="&treehead.timeLastUsed.label;" flex="20"
+ data-field-name="timeLastUsed" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timePasswordChangedCol" label="&treehead.timePasswordChanged.label;" flex="10"
+ data-field-name="timePasswordChanged" persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timesUsedCol" label="&treehead.timesUsed.label;" flex="1"
+ data-field-name="timesUsed" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <separator class="thin"/>
+ <hbox id="SignonViewerButtons">
+ <button id="removeSignon" disabled="true" icon="remove"
+ label="&remove.label;" accesskey="&remove.accesskey;"
+ oncommand="DeleteSignon();"/>
+ <button id="removeAllSignons" icon="clear"
+ label="&removeall.label;" accesskey="&removeall.accesskey;"
+ oncommand="DeleteAllSignons();"/>
+ <spacer flex="1"/>
+#if defined(MOZ_BUILD_APP_IS_BROWSER) && defined(XP_WIN)
+ <button accesskey="&import.accesskey;"
+ label="&import.label;"
+ oncommand="OpenMigrator();"/>
+#endif
+ <button id="togglePasswords"
+ oncommand="TogglePasswordVisible();"/>
+ </hbox>
+ </vbox>
+ <hbox align="end">
+ <hbox class="actionButtons" flex="1">
+ <spacer flex="1"/>
+#ifndef XP_MACOSX
+ <button oncommand="close();" icon="close"
+ label="&closebutton.label;" accesskey="&closebutton.accesskey;"/>
+#endif
+ </hbox>
+ </hbox>
+</window>
diff --git a/toolkit/components/passwordmgr/content/recipes.json b/toolkit/components/passwordmgr/content/recipes.json
new file mode 100644
index 000000000..fc747219b
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/recipes.json
@@ -0,0 +1,31 @@
+{
+ "siteRecipes": [
+ {
+ "description": "okta uses a hidden password field to disable filling",
+ "hosts": ["mozilla.okta.com"],
+ "passwordSelector": "#pass-signin"
+ },
+ {
+ "description": "anthem uses a hidden password and username field to disable filling",
+ "hosts": ["www.anthem.com"],
+ "passwordSelector": "#LoginContent_txtLoginPass"
+ },
+ {
+ "description": "An ephemeral password-shim field is incorrectly selected as the username field.",
+ "hosts": ["www.discover.com"],
+ "usernameSelector": "#login-account"
+ },
+ {
+ "description": "Tibia uses type=password for its username field and puts the email address before the password field during registration",
+ "hosts": ["secure.tibia.com"],
+ "usernameSelector": "#accountname, input[name='loginname']",
+ "passwordSelector": "#password1, input[name='loginpassword']",
+ "pathRegex": "^\/account\/"
+ },
+ {
+ "description": "Username field will be incorrectly captured in the change password form (bug 1243722)",
+ "hosts": ["www.facebook.com"],
+ "notUsernameSelector": "#password_strength"
+ }
+ ]
+}
diff --git a/toolkit/components/passwordmgr/crypto-SDR.js b/toolkit/components/passwordmgr/crypto-SDR.js
new file mode 100644
index 000000000..b0916eb29
--- /dev/null
+++ b/toolkit/components/passwordmgr/crypto-SDR.js
@@ -0,0 +1,207 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+function LoginManagerCrypto_SDR() {
+ this.init();
+}
+
+LoginManagerCrypto_SDR.prototype = {
+
+ classID : Components.ID("{dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerCrypto]),
+
+ __sdrSlot : null, // PKCS#11 slot being used by the SDR.
+ get _sdrSlot() {
+ if (!this.__sdrSlot) {
+ let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"].
+ getService(Ci.nsIPKCS11ModuleDB);
+ this.__sdrSlot = modules.findSlotByName("");
+ }
+ return this.__sdrSlot;
+ },
+
+ __decoderRing : null, // nsSecretDecoderRing service
+ get _decoderRing() {
+ if (!this.__decoderRing)
+ this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].
+ getService(Ci.nsISecretDecoderRing);
+ return this.__decoderRing;
+ },
+
+ __utfConverter : null, // UCS2 <--> UTF8 string conversion
+ get _utfConverter() {
+ if (!this.__utfConverter) {
+ this.__utfConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ this.__utfConverter.charset = "UTF-8";
+ }
+ return this.__utfConverter;
+ },
+
+ _utfConverterReset : function() {
+ this.__utfConverter = null;
+ },
+
+ _uiBusy : false,
+
+
+ init : function () {
+ // Check to see if the internal PKCS#11 token has been initialized.
+ // If not, set a blank password.
+ let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].
+ getService(Ci.nsIPK11TokenDB);
+
+ let token = tokenDB.getInternalKeyToken();
+ if (token.needsUserInit) {
+ this.log("Initializing key3.db with default blank password.");
+ token.initPassword("");
+ }
+ },
+
+
+ /*
+ * encrypt
+ *
+ * Encrypts the specified string, using the SecretDecoderRing.
+ *
+ * Returns the encrypted string, or throws an exception if there was a
+ * problem.
+ */
+ encrypt : function (plainText) {
+ let cipherText = null;
+
+ let wasLoggedIn = this.isLoggedIn;
+ let canceledMP = false;
+
+ this._uiBusy = true;
+ try {
+ let plainOctet = this._utfConverter.ConvertFromUnicode(plainText);
+ plainOctet += this._utfConverter.Finish();
+ cipherText = this._decoderRing.encryptString(plainOctet);
+ } catch (e) {
+ this.log("Failed to encrypt string. (" + e.name + ")");
+ // If the user clicks Cancel, we get NS_ERROR_FAILURE.
+ // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
+ if (e.result == Cr.NS_ERROR_FAILURE) {
+ canceledMP = true;
+ throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+ } else {
+ throw Components.Exception("Couldn't encrypt string", Cr.NS_ERROR_FAILURE);
+ }
+ } finally {
+ this._uiBusy = false;
+ // If we triggered a master password prompt, notify observers.
+ if (!wasLoggedIn && this.isLoggedIn)
+ this._notifyObservers("passwordmgr-crypto-login");
+ else if (canceledMP)
+ this._notifyObservers("passwordmgr-crypto-loginCanceled");
+ }
+ return cipherText;
+ },
+
+
+ /*
+ * decrypt
+ *
+ * Decrypts the specified string, using the SecretDecoderRing.
+ *
+ * Returns the decrypted string, or throws an exception if there was a
+ * problem.
+ */
+ decrypt : function (cipherText) {
+ let plainText = null;
+
+ let wasLoggedIn = this.isLoggedIn;
+ let canceledMP = false;
+
+ this._uiBusy = true;
+ try {
+ let plainOctet;
+ plainOctet = this._decoderRing.decryptString(cipherText);
+ plainText = this._utfConverter.ConvertToUnicode(plainOctet);
+ } catch (e) {
+ this.log("Failed to decrypt string: " + cipherText +
+ " (" + e.name + ")");
+
+ // In the unlikely event the converter threw, reset it.
+ this._utfConverterReset();
+
+ // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
+ // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
+ // Wrong passwords are handled by the decoderRing reprompting;
+ // we get no notification.
+ if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ canceledMP = true;
+ throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+ } else {
+ throw Components.Exception("Couldn't decrypt string", Cr.NS_ERROR_FAILURE);
+ }
+ } finally {
+ this._uiBusy = false;
+ // If we triggered a master password prompt, notify observers.
+ if (!wasLoggedIn && this.isLoggedIn)
+ this._notifyObservers("passwordmgr-crypto-login");
+ else if (canceledMP)
+ this._notifyObservers("passwordmgr-crypto-loginCanceled");
+ }
+
+ return plainText;
+ },
+
+
+ /*
+ * uiBusy
+ */
+ get uiBusy() {
+ return this._uiBusy;
+ },
+
+
+ /*
+ * isLoggedIn
+ */
+ get isLoggedIn() {
+ let status = this._sdrSlot.status;
+ this.log("SDR slot status is " + status);
+ if (status == Ci.nsIPKCS11Slot.SLOT_READY ||
+ status == Ci.nsIPKCS11Slot.SLOT_LOGGED_IN)
+ return true;
+ if (status == Ci.nsIPKCS11Slot.SLOT_NOT_LOGGED_IN)
+ return false;
+ throw Components.Exception("unexpected slot status: " + status, Cr.NS_ERROR_FAILURE);
+ },
+
+
+ /*
+ * defaultEncType
+ */
+ get defaultEncType() {
+ return Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
+ },
+
+
+ /*
+ * _notifyObservers
+ */
+ _notifyObservers : function(topic) {
+ this.log("Prompted for a master password, notifying for " + topic);
+ Services.obs.notifyObservers(null, topic, null);
+ },
+}; // end of nsLoginManagerCrypto_SDR implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerCrypto_SDR.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login crypto");
+ return logger.log.bind(logger);
+});
+
+var component = [LoginManagerCrypto_SDR];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/passwordmgr/jar.mn b/toolkit/components/passwordmgr/jar.mn
new file mode 100644
index 000000000..9fa574e49
--- /dev/null
+++ b/toolkit/components/passwordmgr/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content passwordmgr %content/passwordmgr/
+* content/passwordmgr/passwordManager.xul (content/passwordManager.xul)
+ content/passwordmgr/passwordManager.js (content/passwordManager.js)
+ content/passwordmgr/recipes.json (content/recipes.json)
diff --git a/toolkit/components/passwordmgr/moz.build b/toolkit/components/passwordmgr/moz.build
new file mode 100644
index 000000000..72c8c70a4
--- /dev/null
+++ b/toolkit/components/passwordmgr/moz.build
@@ -0,0 +1,78 @@
+# -*- 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/.
+
+if CONFIG['MOZ_BUILD_APP'] == 'browser':
+ DEFINES['MOZ_BUILD_APP_IS_BROWSER'] = True
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini', 'test/mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+TESTING_JS_MODULES += [
+ # Make this file available from the "resource:" URI of the test environment.
+ 'test/browser/form_basic.html',
+ 'test/LoginTestUtils.jsm',
+]
+
+XPIDL_SOURCES += [
+ 'nsILoginInfo.idl',
+ 'nsILoginManager.idl',
+ 'nsILoginManagerCrypto.idl',
+ 'nsILoginManagerPrompter.idl',
+ 'nsILoginManagerStorage.idl',
+ 'nsILoginMetaInfo.idl',
+]
+
+XPIDL_MODULE = 'loginmgr'
+
+EXTRA_COMPONENTS += [
+ 'crypto-SDR.js',
+ 'nsLoginInfo.js',
+ 'nsLoginManager.js',
+ 'nsLoginManagerPrompter.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'passwordmgr.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'InsecurePasswordUtils.jsm',
+ 'LoginHelper.jsm',
+ 'LoginManagerContent.jsm',
+ 'LoginManagerParent.jsm',
+ 'LoginRecipes.jsm',
+ 'OSCrypto.jsm',
+]
+
+if CONFIG['OS_TARGET'] == 'Android':
+ EXTRA_COMPONENTS += [
+ 'storage-mozStorage.js',
+ ]
+else:
+ EXTRA_COMPONENTS += [
+ 'storage-json.js',
+ ]
+ EXTRA_JS_MODULES += [
+ 'LoginImport.jsm',
+ 'LoginStore.jsm',
+ ]
+
+if CONFIG['OS_TARGET'] == 'WINNT':
+ EXTRA_JS_MODULES += [
+ 'OSCrypto_win.js',
+ ]
+
+if CONFIG['MOZ_BUILD_APP'] == 'browser':
+ EXTRA_JS_MODULES += [
+ 'LoginManagerContextMenu.jsm',
+ ]
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Password Manager')
diff --git a/toolkit/components/passwordmgr/nsILoginInfo.idl b/toolkit/components/passwordmgr/nsILoginInfo.idl
new file mode 100644
index 000000000..7dce9033d
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginInfo.idl
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(c41b7dff-6b9b-42fe-b78d-113051facb05)]
+
+/**
+ * An object containing information for a login stored by the
+ * password manager.
+ */
+interface nsILoginInfo : nsISupports {
+ /**
+ * The hostname the login applies to.
+ *
+ * The hostname should be formatted as an URL. For example,
+ * "https://site.com", "http://site.com:1234", "ftp://ftp.site.com".
+ */
+ attribute AString hostname;
+
+ /**
+ * The URL a form-based login was submitted to.
+ *
+ * For logins obtained from HTML forms, this field is the |action|
+ * attribute from the |form| element, with the path removed. For
+ * example "http://www.site.com". [Forms with no |action| attribute
+ * default to submitting to their origin URL, so we store that.]
+ *
+ * For logins obtained from a HTTP or FTP protocol authentication,
+ * this field is NULL.
+ */
+ attribute AString formSubmitURL;
+
+ /**
+ * The HTTP Realm a login was requested for.
+ *
+ * When an HTTP server sends a 401 result, the WWW-Authenticate
+ * header includes a realm to identify the "protection space." See
+ * RFC2617. If the response sent has a missing or blank realm, the
+ * hostname is used instead.
+ *
+ * For logins obtained from HTML forms, this field is NULL.
+ */
+ attribute AString httpRealm;
+
+ /**
+ * The username for the login.
+ */
+ attribute AString username;
+
+ /**
+ * The |name| attribute for the username input field.
+ *
+ * For logins obtained from a HTTP or FTP protocol authentication,
+ * this field is an empty string.
+ */
+ attribute AString usernameField;
+
+ /**
+ * The password for the login.
+ */
+ attribute AString password;
+
+ /**
+ * The |name| attribute for the password input field.
+ *
+ * For logins obtained from a HTTP or FTP protocol authentication,
+ * this field is an empty string.
+ */
+ attribute AString passwordField;
+
+ /**
+ * Initialize a newly created nsLoginInfo object.
+ *
+ * The arguments are the fields for the new object.
+ */
+ void init(in AString aHostname,
+ in AString aFormSubmitURL, in AString aHttpRealm,
+ in AString aUsername, in AString aPassword,
+ in AString aUsernameField, in AString aPasswordField);
+
+ /**
+ * Test for strict equality with another nsILoginInfo object.
+ *
+ * @param aLoginInfo
+ * The other object to test.
+ */
+ boolean equals(in nsILoginInfo aLoginInfo);
+
+ /**
+ * Test for loose equivalency with another nsILoginInfo object. The
+ * passwordField and usernameField values are ignored, and the password
+ * values may be optionally ignored. If one login's formSubmitURL is an
+ * empty string (but not null), it will be treated as a wildcard. [The
+ * blank value indicates the login was stored before bug 360493 was fixed.]
+ *
+ * @param aLoginInfo
+ * The other object to test.
+ * @param ignorePassword
+ * If true, ignore the password when checking for match.
+ */
+ boolean matches(in nsILoginInfo aLoginInfo, in boolean ignorePassword);
+
+ /**
+ * Create an identical copy of the login, duplicating all of the login's
+ * nsILoginInfo and nsILoginMetaInfo properties.
+ *
+ * This allows code to be forwards-compatible, when additional properties
+ * are added to nsILoginMetaInfo (or nsILoginInfo) in the future.
+ */
+ nsILoginInfo clone();
+};
+
+%{C++
+
+#define NS_LOGININFO_CONTRACTID "@mozilla.org/login-manager/loginInfo;1"
+
+%}
diff --git a/toolkit/components/passwordmgr/nsILoginManager.idl b/toolkit/components/passwordmgr/nsILoginManager.idl
new file mode 100644
index 000000000..30b5a0449
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManager.idl
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsILoginInfo;
+interface nsIAutoCompleteResult;
+interface nsIFormAutoCompleteObserver;
+interface nsIDOMHTMLInputElement;
+interface nsIDOMHTMLFormElement;
+interface nsIPropertyBag;
+
+[scriptable, uuid(38c7f6af-7df9-49c7-b558-2776b24e6cc1)]
+interface nsILoginManager : nsISupports {
+ /**
+ * This promise is resolved when initialization is complete, and is rejected
+ * in case initialization failed. This includes the initial loading of the
+ * login data as well as any migration from previous versions.
+ *
+ * Calling any method of nsILoginManager before this promise is resolved
+ * might trigger the synchronous initialization fallback.
+ */
+ readonly attribute jsval initializationPromise;
+
+
+ /**
+ * Store a new login in the login manager.
+ *
+ * @param aLogin
+ * The login to be added.
+ * @return a clone of the login info with the guid set (even if it was not provided)
+ *
+ * Default values for the login's nsILoginMetaInfo properties will be
+ * created. However, if the caller specifies non-default values, they will
+ * be used instead.
+ */
+ nsILoginInfo addLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Remove a login from the login manager.
+ *
+ * @param aLogin
+ * The login to be removed.
+ *
+ * The specified login must exactly match a stored login. However, the
+ * values of any nsILoginMetaInfo properties are ignored.
+ */
+ void removeLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Modify an existing login in the login manager.
+ *
+ * @param oldLogin
+ * The login to be modified.
+ * @param newLoginData
+ * The new login values (either a nsILoginInfo or nsIProperyBag)
+ *
+ * If newLoginData is a nsILoginInfo, all of the old login's nsILoginInfo
+ * properties are changed to the values from newLoginData (but the old
+ * login's nsILoginMetaInfo properties are unmodified).
+ *
+ * If newLoginData is a nsIPropertyBag, only the specified properties
+ * will be changed. The nsILoginMetaInfo properties of oldLogin can be
+ * changed in this manner.
+ *
+ * If the propertybag contains an item named "timesUsedIncrement", the
+ * login's timesUsed property will be incremented by the item's value.
+ */
+ void modifyLogin(in nsILoginInfo oldLogin, in nsISupports newLoginData);
+
+
+ /**
+ * Remove all logins known to login manager.
+ *
+ * The browser sanitization feature allows the user to clear any stored
+ * passwords. This interface allows that to be done without getting each
+ * login first (which might require knowing the master password).
+ *
+ */
+ void removeAllLogins();
+
+
+ /**
+ * Fetch all logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property and omit this param.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.getAllLogins();
+ * (|logins| is an array).
+ */
+ void getAllLogins([optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Obtain a list of all hosts for which password saving is disabled.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property and omit this param.
+ * @param hostnames
+ * An array of hostname strings, in origin URL format without a
+ * pathname. For example: "https://www.site.com".
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.getDisabledAllLogins();
+ */
+ void getAllDisabledHosts([optional] out unsigned long count,
+ [retval, array, size_is(count)] out wstring hostnames);
+
+
+ /**
+ * Check to see if saving logins has been disabled for a host.
+ *
+ * @param aHost
+ * The hostname to check. This argument should be in the origin
+ * URL format, without a pathname. For example: "http://foo.com".
+ */
+ boolean getLoginSavingEnabled(in AString aHost);
+
+
+ /**
+ * Disable (or enable) storing logins for the specified host. When
+ * disabled, the login manager will not prompt to store logins for
+ * that host. Existing logins are not affected.
+ *
+ * @param aHost
+ * The hostname to set. This argument should be in the origin
+ * URL format, without a pathname. For example: "http://foo.com".
+ * @param isEnabled
+ * Specify if saving logins should be enabled (true) or
+ * disabled (false)
+ */
+ void setLoginSavingEnabled(in AString aHost, in boolean isEnabled);
+
+
+ /**
+ * Search for logins matching the specified criteria. Called when looking
+ * for logins that might be applicable to a form or authentication request.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |findLogins({}, hostname, ...)|
+ * @param aHostname
+ * The hostname to restrict searches to, in URL format. For
+ * example: "http://www.site.com".
+ * To find logins for a given nsIURI, you would typically pass in
+ * its prePath.
+ * @param aActionURL
+ * For form logins, this argument should be the URL to which the
+ * form will be submitted. For protocol logins, specify null.
+ * An empty string ("") will match any value (except null).
+ * @param aHttpRealm
+ * For protocol logins, this argument should be the HTTP Realm
+ * for which the login applies. This is obtained from the
+ * WWW-Authenticate header. See RFC2617. For form logins,
+ * specify null.
+ * An empty string ("") will match any value (except null).
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.findLogins({}, hostname, ...);
+ *
+ */
+ void findLogins(out unsigned long count, in AString aHostname,
+ in AString aActionURL, in AString aHttpRealm,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins matching the specified criteria, as with
+ * findLogins(). This interface only returns the number of matching
+ * logins (and not the logins themselves), which allows a caller to
+ * check for logins without causing the user to be prompted for a master
+ * password to decrypt the logins.
+ *
+ * @param aHostname
+ * The hostname to restrict searches to. Specify an empty string
+ * to match all hosts. A null value will not match any logins, and
+ * will thus always return a count of 0.
+ * @param aActionURL
+ * The URL to which a form login will be submitted. To match any
+ * form login, specify an empty string. To not match any form
+ * login, specify null.
+ * @param aHttpRealm
+ * The HTTP Realm for which the login applies. To match logins for
+ * any realm, specify an empty string. To not match logins for any
+ * realm, specify null.
+ */
+ unsigned long countLogins(in AString aHostname, in AString aActionURL,
+ in AString aHttpRealm);
+
+
+ /**
+ * Generate results for a userfield autocomplete menu.
+ *
+ * NOTE: This interface is provided for use only by the FormFillController,
+ * which calls it directly. This isn't really ideal, it should
+ * probably be callback registered through the FFC.
+ */
+ void autoCompleteSearchAsync(in AString aSearchString,
+ in nsIAutoCompleteResult aPreviousResult,
+ in nsIDOMHTMLInputElement aElement,
+ in nsIFormAutoCompleteObserver aListener);
+
+ /**
+ * Stop a previously-started async search.
+ */
+ void stopSearch();
+
+ /**
+ * Search for logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |searchLogins({}, matchData)|
+ * @param matchData
+ * The data used to search. This does not follow the same
+ * requirements as findLogins for those fields. Wildcard matches are
+ * simply not specified.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.searchLogins({}, matchData);
+ * (|logins| is an array).
+ */
+ void searchLogins(out unsigned long count, in nsIPropertyBag matchData,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+ /**
+ * True when a master password prompt is being displayed.
+ */
+ readonly attribute boolean uiBusy;
+
+ /**
+ * True when the master password has already been entered, and so a caller
+ * can ask for decrypted logins without triggering a prompt.
+ */
+ readonly attribute boolean isLoggedIn;
+};
+
+%{C++
+
+#define NS_LOGINMANAGER_CONTRACTID "@mozilla.org/login-manager;1"
+
+%}
diff --git a/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl b/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
new file mode 100644
index 000000000..8af36a258
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(2030770e-542e-40cd-8061-cd9d4ad4227f)]
+
+interface nsILoginManagerCrypto : nsISupports {
+
+ const unsigned long ENCTYPE_BASE64 = 0; // obsolete
+ const unsigned long ENCTYPE_SDR = 1;
+
+ /**
+ * encrypt
+ *
+ * @param plainText
+ * The string to be encrypted.
+ *
+ * Encrypts the specified string, returning the ciphertext value.
+ *
+ * NOTE: The current implemention of this inferface simply uses NSS/PSM's
+ * "Secret Decoder Ring" service. It is not recommended for general
+ * purpose encryption/decryption.
+ *
+ * Can throw if the user cancels entry of their master password.
+ */
+ AString encrypt(in AString plainText);
+
+ /**
+ * decrypt
+ *
+ * @param cipherText
+ * The string to be decrypted.
+ *
+ * Decrypts the specified string, returning the plaintext value.
+ *
+ * Can throw if the user cancels entry of their master password, or if the
+ * cipherText value can not be successfully decrypted (eg, if it was
+ * encrypted with some other key).
+ */
+ AString decrypt(in AString cipherText);
+
+ /**
+ * uiBusy
+ *
+ * True when a master password prompt is being displayed.
+ */
+ readonly attribute boolean uiBusy;
+
+ /**
+ * isLoggedIn
+ *
+ * Current login state of the token used for encryption. If the user is
+ * not logged in, performing a crypto operation will result in a master
+ * password prompt.
+ */
+ readonly attribute boolean isLoggedIn;
+
+ /**
+ * defaultEncType
+ *
+ * Default encryption type used by an implementation of this interface.
+ */
+ readonly attribute unsigned long defaultEncType;
+};
diff --git a/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl b/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl
new file mode 100644
index 000000000..c673154d1
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface nsILoginInfo;
+interface nsIDOMElement;
+interface nsIDOMWindow;
+
+[scriptable, uuid(425f73b9-b2db-4e8a-88c5-9ac2512934ce)]
+interface nsILoginManagerPrompter : nsISupports {
+ /**
+ * Initialize the prompter. Must be called before using other interfaces.
+ *
+ * @param aWindow
+ * The window in which the user is doing some login-related action that's
+ * resulting in a need to prompt them for something. The prompt
+ * will be associated with this window (or, if a notification bar
+ * is being used, topmost opener in some cases).
+ *
+ * aWindow can be null if there is no associated window, e.g. in a JSM
+ * or Sandbox. In this case there will be no checkbox to save the login
+ * since the window is needed to know if this is a private context.
+ *
+ * If this window is a content window, the corresponding window and browser
+ * elements will be calculated. If this window is a chrome window, the
+ * corresponding browser element needs to be set using setBrowser.
+ */
+ void init(in nsIDOMWindow aWindow);
+
+ /**
+ * The browser this prompter is being created for.
+ * This is required if the init function received a chrome window as argument.
+ */
+ attribute nsIDOMElement browser;
+
+ /**
+ * The opener that was used to open the window passed to init.
+ * The opener can be used to determine in which window the prompt
+ * should be shown. Must be a content window that is not a frame window,
+ * make sure to pass the top window using e.g. window.top.
+ */
+ attribute nsIDOMWindow opener;
+
+ /**
+ * Ask the user if they want to save a login (Yes, Never, Not Now)
+ *
+ * @param aLogin
+ * The login to be saved.
+ */
+ void promptToSavePassword(in nsILoginInfo aLogin);
+
+ /**
+ * Ask the user if they want to change a login's password or username.
+ * If the user consents, modifyLogin() will be called.
+ *
+ * @param aOldLogin
+ * The existing login (with the old password).
+ * @param aNewLogin
+ * The new login.
+ */
+ void promptToChangePassword(in nsILoginInfo aOldLogin,
+ in nsILoginInfo aNewLogin);
+
+ /**
+ * Ask the user if they want to change the password for one of
+ * multiple logins, when the caller can't determine exactly which
+ * login should be changed. If the user consents, modifyLogin() will
+ * be called.
+ *
+ * @param logins
+ * An array of existing logins.
+ * @param count
+ * (length of the array)
+ * @param aNewLogin
+ * The new login.
+ *
+ * Note: Because the caller does not know the username of the login
+ * to be changed, aNewLogin.username and aNewLogin.usernameField
+ * will be set (using the user's selection) before modifyLogin()
+ * is called.
+ */
+ void promptToChangePasswordWithUsernames(
+ [array, size_is(count)] in nsILoginInfo logins,
+ in uint32_t count,
+ in nsILoginInfo aNewLogin);
+};
+%{C++
+
+#define NS_LOGINMANAGERPROMPTER_CONTRACTID "@mozilla.org/login-manager/prompter/;1"
+
+%}
diff --git a/toolkit/components/passwordmgr/nsILoginManagerStorage.idl b/toolkit/components/passwordmgr/nsILoginManagerStorage.idl
new file mode 100644
index 000000000..4ad3dbfe9
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManagerStorage.idl
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsILoginInfo;
+interface nsIPropertyBag;
+
+[scriptable, uuid(5df81a93-25e6-4b45-a696-089479e15c7d)]
+
+/*
+ * NOTE: This interface is intended to be implemented by modules
+ * providing storage mechanisms for the login manager.
+ * Other code should use the login manager's interfaces
+ * (nsILoginManager), and should not call storage modules
+ * directly.
+ */
+interface nsILoginManagerStorage : nsISupports {
+ /**
+ * Initialize the component.
+ *
+ * At present, other methods of this interface may be called before the
+ * returned promise is resolved or rejected.
+ *
+ * @return {Promise}
+ * @resolves When initialization is complete.
+ * @rejects JavaScript exception.
+ */
+ jsval initialize();
+
+
+ /**
+ * Ensures that all data has been written to disk and all files are closed.
+ *
+ * At present, this method is called by regression tests only. Finalization
+ * on shutdown is done by observers within the component.
+ *
+ * @return {Promise}
+ * @resolves When finalization is complete.
+ * @rejects JavaScript exception.
+ */
+ jsval terminate();
+
+
+ /**
+ * Store a new login in the storage module.
+ *
+ * @param aLogin
+ * The login to be added.
+ * @return a clone of the login info with the guid set (even if it was not provided).
+ *
+ * Default values for the login's nsILoginMetaInfo properties will be
+ * created. However, if the caller specifies non-default values, they will
+ * be used instead.
+ */
+ nsILoginInfo addLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Remove a login from the storage module.
+ *
+ * @param aLogin
+ * The login to be removed.
+ *
+ * The specified login must exactly match a stored login. However, the
+ * values of any nsILoginMetaInfo properties are ignored.
+ */
+ void removeLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Modify an existing login in the storage module.
+ *
+ * @param oldLogin
+ * The login to be modified.
+ * @param newLoginData
+ * The new login values (either a nsILoginInfo or nsIProperyBag)
+ *
+ * If newLoginData is a nsILoginInfo, all of the old login's nsILoginInfo
+ * properties are changed to the values from newLoginData (but the old
+ * login's nsILoginMetaInfo properties are unmodified).
+ *
+ * If newLoginData is a nsIPropertyBag, only the specified properties
+ * will be changed. The nsILoginMetaInfo properties of oldLogin can be
+ * changed in this manner.
+ *
+ * If the propertybag contains an item named "timesUsedIncrement", the
+ * login's timesUsed property will be incremented by the item's value.
+ */
+ void modifyLogin(in nsILoginInfo oldLogin, in nsISupports newLoginData);
+
+
+ /**
+ * Remove all stored logins.
+ *
+ * The browser sanitization feature allows the user to clear any stored
+ * passwords. This interface allows that to be done without getting each
+ * login first (which might require knowing the master password).
+ *
+ */
+ void removeAllLogins();
+
+
+ /**
+ * Fetch all logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property and omit this param.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.getAllLogins();
+ * (|logins| is an array).
+ */
+ void getAllLogins([optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |searchLogins({}, matchData)|
+ * @param matchData
+ * The data used to search. This does not follow the same
+ * requirements as findLogins for those fields. Wildcard matches are
+ * simply not specified.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.searchLogins({}, matchData);
+ * (|logins| is an array).
+ */
+ void searchLogins(out unsigned long count, in nsIPropertyBag matchData,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins matching the specified criteria. Called when looking
+ * for logins that might be applicable to a form or authentication request.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |findLogins({}, hostname, ...)|
+ * @param aHostname
+ * The hostname to restrict searches to, in URL format. For
+ * example: "http://www.site.com".
+ * @param aActionURL
+ * For form logins, this argument should be the URL to which the
+ * form will be submitted. For protocol logins, specify null.
+ * @param aHttpRealm
+ * For protocol logins, this argument should be the HTTP Realm
+ * for which the login applies. This is obtained from the
+ * WWW-Authenticate header. See RFC2617. For form logins,
+ * specify null.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.findLogins({}, hostname, ...);
+ *
+ */
+ void findLogins(out unsigned long count, in AString aHostname,
+ in AString aActionURL, in AString aHttpRealm,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins matching the specified criteria, as with
+ * findLogins(). This interface only returns the number of matching
+ * logins (and not the logins themselves), which allows a caller to
+ * check for logins without causing the user to be prompted for a master
+ * password to decrypt the logins.
+ *
+ * @param aHostname
+ * The hostname to restrict searches to. Specify an empty string
+ * to match all hosts. A null value will not match any logins, and
+ * will thus always return a count of 0.
+ * @param aActionURL
+ * The URL to which a form login will be submitted. To match any
+ * form login, specify an empty string. To not match any form
+ * login, specify null.
+ * @param aHttpRealm
+ * The HTTP Realm for which the login applies. To match logins for
+ * any realm, specify an empty string. To not match logins for any
+ * realm, specify null.
+ */
+ unsigned long countLogins(in AString aHostname, in AString aActionURL,
+ in AString aHttpRealm);
+ /**
+ * True when a master password prompt is being shown.
+ */
+ readonly attribute boolean uiBusy;
+
+ /**
+ * True when the master password has already been entered, and so a caller
+ * can ask for decrypted logins without triggering a prompt.
+ */
+ readonly attribute boolean isLoggedIn;
+};
diff --git a/toolkit/components/passwordmgr/nsILoginMetaInfo.idl b/toolkit/components/passwordmgr/nsILoginMetaInfo.idl
new file mode 100644
index 000000000..92d8f2bc8
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginMetaInfo.idl
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(20d8eb40-c494-497f-b2a6-aaa32f807ebd)]
+
+/**
+ * An object containing metainfo for a login stored by the login manager.
+ *
+ * Code using login manager can generally ignore this interface. When adding
+ * logins, default value will be created. When modifying logins, these
+ * properties will be unchanged unless a change is explicitly requested [by
+ * using modifyLogin() with a nsIPropertyBag]. When deleting a login or
+ * comparing logins, these properties are ignored.
+ */
+interface nsILoginMetaInfo : nsISupports {
+ /**
+ * The GUID to uniquely identify the login. This can be any arbitrary
+ * string, but a format as created by nsIUUIDGenerator is recommended.
+ * For example, "{d4e1a1f6-5ea0-40ee-bff5-da57982f21cf}"
+ *
+ * addLogin will generate a random value unless a value is provided.
+ *
+ * addLogin and modifyLogin will throw if the GUID already exists.
+ */
+ attribute AString guid;
+
+ /**
+ * The time, in Unix Epoch milliseconds, when the login was first created.
+ */
+ attribute unsigned long long timeCreated;
+
+ /**
+ * The time, in Unix Epoch milliseconds, when the login was last submitted
+ * in a form or used to begin an HTTP auth session.
+ */
+ attribute unsigned long long timeLastUsed;
+
+ /**
+ * The time, in Unix Epoch milliseconds, when the login was last modified.
+ *
+ * Contrary to what the name may suggest, this attribute takes into account
+ * not only the password but also the username attribute.
+ */
+ attribute unsigned long long timePasswordChanged;
+
+ /**
+ * The number of times the login was submitted in a form or used to begin
+ * an HTTP auth session.
+ */
+ attribute unsigned long timesUsed;
+};
diff --git a/toolkit/components/passwordmgr/nsLoginInfo.js b/toolkit/components/passwordmgr/nsLoginInfo.js
new file mode 100644
index 000000000..d6ea86446
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsLoginInfo.js
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+
+function nsLoginInfo() {}
+
+nsLoginInfo.prototype = {
+
+ classID : Components.ID("{0f2f347c-1e4f-40cc-8efd-792dea70a85e}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginInfo, Ci.nsILoginMetaInfo]),
+
+ //
+ // nsILoginInfo interfaces...
+ //
+
+ hostname : null,
+ formSubmitURL : null,
+ httpRealm : null,
+ username : null,
+ password : null,
+ usernameField : null,
+ passwordField : null,
+
+ init : function (aHostname, aFormSubmitURL, aHttpRealm,
+ aUsername, aPassword,
+ aUsernameField, aPasswordField) {
+ this.hostname = aHostname;
+ this.formSubmitURL = aFormSubmitURL;
+ this.httpRealm = aHttpRealm;
+ this.username = aUsername;
+ this.password = aPassword;
+ this.usernameField = aUsernameField;
+ this.passwordField = aPasswordField;
+ },
+
+ matches(aLogin, ignorePassword) {
+ return LoginHelper.doLoginsMatch(this, aLogin, {
+ ignorePassword,
+ });
+ },
+
+ equals : function (aLogin) {
+ if (this.hostname != aLogin.hostname ||
+ this.formSubmitURL != aLogin.formSubmitURL ||
+ this.httpRealm != aLogin.httpRealm ||
+ this.username != aLogin.username ||
+ this.password != aLogin.password ||
+ this.usernameField != aLogin.usernameField ||
+ this.passwordField != aLogin.passwordField)
+ return false;
+
+ return true;
+ },
+
+ clone : function() {
+ let clone = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ clone.init(this.hostname, this.formSubmitURL, this.httpRealm,
+ this.username, this.password,
+ this.usernameField, this.passwordField);
+
+ // Copy nsILoginMetaInfo props
+ clone.QueryInterface(Ci.nsILoginMetaInfo);
+ clone.guid = this.guid;
+ clone.timeCreated = this.timeCreated;
+ clone.timeLastUsed = this.timeLastUsed;
+ clone.timePasswordChanged = this.timePasswordChanged;
+ clone.timesUsed = this.timesUsed;
+
+ return clone;
+ },
+
+ //
+ // nsILoginMetaInfo interfaces...
+ //
+
+ guid : null,
+ timeCreated : null,
+ timeLastUsed : null,
+ timePasswordChanged : null,
+ timesUsed : null
+
+}; // end of nsLoginInfo implementation
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsLoginInfo]);
diff --git a/toolkit/components/passwordmgr/nsLoginManager.js b/toolkit/components/passwordmgr/nsLoginManager.js
new file mode 100644
index 000000000..514351fa5
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -0,0 +1,541 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/LoginManagerContent.jsm"); /* global UserAutoCompleteResult */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory",
+ "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+ "resource://gre/modules/InsecurePasswordUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("nsLoginManager");
+ return logger;
+});
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+function LoginManager() {
+ this.init();
+}
+
+LoginManager.prototype = {
+
+ classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManager,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIInterfaceRequestor]),
+ getInterface(aIID) {
+ if (aIID.equals(Ci.mozIStorageConnection) && this._storage) {
+ let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor);
+ return ir.getInterface(aIID);
+ }
+
+ if (aIID.equals(Ci.nsIVariant)) {
+ // Allows unwrapping the JavaScript object for regression tests.
+ return this;
+ }
+
+ throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+
+ /* ---------- private members ---------- */
+
+
+ __formFillService: null, // FormFillController, for username autocompleting
+ get _formFillService() {
+ if (!this.__formFillService) {
+ this.__formFillService = Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(Ci.nsIFormFillController);
+ }
+ return this.__formFillService;
+ },
+
+
+ _storage: null, // Storage component which contains the saved logins
+ _prefBranch: null, // Preferences service
+ _remember: true, // mirrors signon.rememberSignons preference
+
+
+ /**
+ * Initialize the Login Manager. Automatically called when service
+ * is created.
+ *
+ * Note: Service created in /browser/base/content/browser.js,
+ * delayedStartup()
+ */
+ init() {
+
+ // Cache references to current |this| in utility objects
+ this._observer._pwmgr = this;
+
+ // Preferences. Add observer so we get notified of changes.
+ this._prefBranch = Services.prefs.getBranch("signon.");
+ this._prefBranch.addObserver("rememberSignons", this._observer, false);
+
+ this._remember = this._prefBranch.getBoolPref("rememberSignons");
+ this._autoCompleteLookupPromise = null;
+
+ // Form submit observer checks forms for new logins and pw changes.
+ Services.obs.addObserver(this._observer, "xpcom-shutdown", false);
+
+ if (Services.appinfo.processType ===
+ Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.obs.addObserver(this._observer, "passwordmgr-storage-replace",
+ false);
+
+ // Initialize storage so that asynchronous data loading can start.
+ this._initStorage();
+ }
+
+ Services.obs.addObserver(this._observer, "gather-telemetry", false);
+ },
+
+
+ _initStorage() {
+ let contractID;
+ if (AppConstants.platform == "android") {
+ contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
+ } else {
+ contractID = "@mozilla.org/login-manager/storage/json;1";
+ }
+ try {
+ let catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ contractID = catMan.getCategoryEntry("login-manager-storage",
+ "nsILoginManagerStorage");
+ log.debug("Found alternate nsILoginManagerStorage with contract ID:", contractID);
+ } catch (e) {
+ log.debug("No alternate nsILoginManagerStorage registered");
+ }
+
+ this._storage = Cc[contractID].
+ createInstance(Ci.nsILoginManagerStorage);
+ this.initializationPromise = this._storage.initialize();
+ },
+
+
+ /* ---------- Utility objects ---------- */
+
+
+ /**
+ * Internal utility object, implements the nsIObserver interface.
+ * Used to receive notification for: form submission, preference changes.
+ */
+ _observer: {
+ _pwmgr: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ // nsIObserver
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ var prefName = data;
+ log.debug("got change to", prefName, "preference");
+
+ if (prefName == "rememberSignons") {
+ this._pwmgr._remember =
+ this._pwmgr._prefBranch.getBoolPref("rememberSignons");
+ } else {
+ log.debug("Oops! Pref not handled, change ignored.");
+ }
+ } else if (topic == "xpcom-shutdown") {
+ delete this._pwmgr.__formFillService;
+ delete this._pwmgr._storage;
+ delete this._pwmgr._prefBranch;
+ this._pwmgr = null;
+ } else if (topic == "passwordmgr-storage-replace") {
+ Task.spawn(function* () {
+ yield this._pwmgr._storage.terminate();
+ this._pwmgr._initStorage();
+ yield this._pwmgr.initializationPromise;
+ Services.obs.notifyObservers(null,
+ "passwordmgr-storage-replace-complete", null);
+ }.bind(this));
+ } else if (topic == "gather-telemetry") {
+ // When testing, the "data" parameter is a string containing the
+ // reference time in milliseconds for time-based statistics.
+ this._pwmgr._gatherTelemetry(data ? parseInt(data)
+ : new Date().getTime());
+ } else {
+ log.debug("Oops! Unexpected notification:", topic);
+ }
+ }
+ },
+
+ /**
+ * Collects statistics about the current logins and settings. The telemetry
+ * histograms used here are not accumulated, but are reset each time this
+ * function is called, since it can be called multiple times in a session.
+ *
+ * This function might also not be called at all in the current session.
+ *
+ * @param referenceTimeMs
+ * Current time used to calculate time-based statistics, expressed as
+ * the number of milliseconds since January 1, 1970, 00:00:00 UTC.
+ * This is set to a fake value during unit testing.
+ */
+ _gatherTelemetry(referenceTimeMs) {
+ function clearAndGetHistogram(histogramId) {
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ histogram.clear();
+ return histogram;
+ }
+
+ clearAndGetHistogram("PWMGR_BLOCKLIST_NUM_SITES").add(
+ this.getAllDisabledHosts({}).length
+ );
+ clearAndGetHistogram("PWMGR_NUM_SAVED_PASSWORDS").add(
+ this.countLogins("", "", "")
+ );
+ clearAndGetHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS").add(
+ this.countLogins("", null, "")
+ );
+
+ // This is a boolean histogram, and not a flag, because we don't want to
+ // record any value if _gatherTelemetry is not called.
+ clearAndGetHistogram("PWMGR_SAVING_ENABLED").add(this._remember);
+
+ // Don't try to get logins if MP is enabled, since we don't want to show a MP prompt.
+ if (!this.isLoggedIn) {
+ return;
+ }
+
+ let logins = this.getAllLogins({});
+
+ let usernamePresentHistogram = clearAndGetHistogram("PWMGR_USERNAME_PRESENT");
+ let loginLastUsedDaysHistogram = clearAndGetHistogram("PWMGR_LOGIN_LAST_USED_DAYS");
+
+ let hostnameCount = new Map();
+ for (let login of logins) {
+ usernamePresentHistogram.add(!!login.username);
+
+ let hostname = login.hostname;
+ hostnameCount.set(hostname, (hostnameCount.get(hostname) || 0 ) + 1);
+
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ let timeLastUsedAgeMs = referenceTimeMs - login.timeLastUsed;
+ if (timeLastUsedAgeMs > 0) {
+ loginLastUsedDaysHistogram.add(
+ Math.floor(timeLastUsedAgeMs / MS_PER_DAY)
+ );
+ }
+ }
+
+ let passwordsCountHistogram = clearAndGetHistogram("PWMGR_NUM_PASSWORDS_PER_HOSTNAME");
+ for (let count of hostnameCount.values()) {
+ passwordsCountHistogram.add(count);
+ }
+ },
+
+
+
+
+
+ /* ---------- Primary Public interfaces ---------- */
+
+
+
+
+ /**
+ * @type Promise
+ * This promise is resolved when initialization is complete, and is rejected
+ * in case the asynchronous part of initialization failed.
+ */
+ initializationPromise: null,
+
+
+ /**
+ * Add a new login to login storage.
+ */
+ addLogin(login) {
+ // Sanity check the login
+ if (login.hostname == null || login.hostname.length == 0) {
+ throw new Error("Can't add a login with a null or empty hostname.");
+ }
+
+ // For logins w/o a username, set to "", not null.
+ if (login.username == null) {
+ throw new Error("Can't add a login with a null username.");
+ }
+
+ if (login.password == null || login.password.length == 0) {
+ throw new Error("Can't add a login with a null or empty password.");
+ }
+
+ if (login.formSubmitURL || login.formSubmitURL == "") {
+ // We have a form submit URL. Can't have a HTTP realm.
+ if (login.httpRealm != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else if (login.httpRealm) {
+ // We have a HTTP realm. Can't have a form submit URL.
+ if (login.formSubmitURL != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else {
+ // Need one or the other!
+ throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
+ }
+
+
+ // Look for an existing entry.
+ var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
+ login.httpRealm);
+
+ if (logins.some(l => login.matches(l, true))) {
+ throw new Error("This login already exists.");
+ }
+
+ log.debug("Adding login");
+ return this._storage.addLogin(login);
+ },
+
+ /**
+ * Remove the specified login from the stored logins.
+ */
+ removeLogin(login) {
+ log.debug("Removing login");
+ return this._storage.removeLogin(login);
+ },
+
+
+ /**
+ * Change the specified login to match the new login.
+ */
+ modifyLogin(oldLogin, newLogin) {
+ log.debug("Modifying login");
+ return this._storage.modifyLogin(oldLogin, newLogin);
+ },
+
+
+ /**
+ * Get a dump of all stored logins. Used by the login manager UI.
+ *
+ * @param count - only needed for XPCOM.
+ * @return {nsILoginInfo[]} - If there are no logins, the array is empty.
+ */
+ getAllLogins(count) {
+ log.debug("Getting a list of all logins");
+ return this._storage.getAllLogins(count);
+ },
+
+
+ /**
+ * Remove all stored logins.
+ */
+ removeAllLogins() {
+ log.debug("Removing all logins");
+ this._storage.removeAllLogins();
+ },
+
+ /**
+ * Get a list of all origins for which logins are disabled.
+ *
+ * @param {Number} count - only needed for XPCOM.
+ *
+ * @return {String[]} of disabled origins. If there are no disabled origins,
+ * the array is empty.
+ */
+ getAllDisabledHosts(count) {
+ log.debug("Getting a list of all disabled origins");
+
+ let disabledHosts = [];
+ let enumerator = Services.perms.enumerator;
+
+ while (enumerator.hasMoreElements()) {
+ let perm = enumerator.getNext();
+ if (perm.type == PERMISSION_SAVE_LOGINS && perm.capability == Services.perms.DENY_ACTION) {
+ disabledHosts.push(perm.principal.URI.prePath);
+ }
+ }
+
+ if (count)
+ count.value = disabledHosts.length; // needed for XPCOM
+
+ log.debug("getAllDisabledHosts: returning", disabledHosts.length, "disabled hosts.");
+ return disabledHosts;
+ },
+
+
+ /**
+ * Search for the known logins for entries matching the specified criteria.
+ */
+ findLogins(count, origin, formActionOrigin, httpRealm) {
+ log.debug("Searching for logins matching origin:", origin,
+ "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm);
+
+ return this._storage.findLogins(count, origin, formActionOrigin,
+ httpRealm);
+ },
+
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins(count, matchData) {
+ log.debug("Searching for logins");
+
+ matchData.QueryInterface(Ci.nsIPropertyBag2);
+ if (!matchData.hasKey("guid")) {
+ if (!matchData.hasKey("hostname")) {
+ log.warn("searchLogins: A `hostname` is recommended");
+ }
+
+ if (!matchData.hasKey("formSubmitURL") && !matchData.hasKey("httpRealm")) {
+ log.warn("searchLogins: `formSubmitURL` or `httpRealm` is recommended");
+ }
+ }
+
+ return this._storage.searchLogins(count, matchData);
+ },
+
+
+ /**
+ * Search for the known logins for entries matching the specified criteria,
+ * returns only the count.
+ */
+ countLogins(origin, formActionOrigin, httpRealm) {
+ log.debug("Counting logins matching origin:", origin,
+ "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm);
+
+ return this._storage.countLogins(origin, formActionOrigin, httpRealm);
+ },
+
+
+ get uiBusy() {
+ return this._storage.uiBusy;
+ },
+
+
+ get isLoggedIn() {
+ return this._storage.isLoggedIn;
+ },
+
+
+ /**
+ * Check to see if user has disabled saving logins for the origin.
+ */
+ getLoginSavingEnabled(origin) {
+ log.debug("Checking if logins to", origin, "can be saved.");
+ if (!this._remember) {
+ return false;
+ }
+
+ let uri = Services.io.newURI(origin, null, null);
+ return Services.perms.testPermission(uri, PERMISSION_SAVE_LOGINS) != Services.perms.DENY_ACTION;
+ },
+
+
+ /**
+ * Enable or disable storing logins for the specified origin.
+ */
+ setLoginSavingEnabled(origin, enabled) {
+ // Throws if there are bogus values.
+ LoginHelper.checkHostnameValue(origin);
+
+ let uri = Services.io.newURI(origin, null, null);
+ if (enabled) {
+ Services.perms.remove(uri, PERMISSION_SAVE_LOGINS);
+ } else {
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ }
+
+ log.debug("Login saving for", origin, "now enabled?", enabled);
+ LoginHelper.notifyStorageChanged(enabled ? "hostSavingEnabled" : "hostSavingDisabled", origin);
+ },
+
+ /**
+ * Yuck. This is called directly by satchel:
+ * nsFormFillController::StartSearch()
+ * [toolkit/components/satchel/nsFormFillController.cpp]
+ *
+ * We really ought to have a simple way for code to register an
+ * auto-complete provider, and not have satchel calling pwmgr directly.
+ */
+ autoCompleteSearchAsync(aSearchString, aPreviousResult,
+ aElement, aCallback) {
+ // aPreviousResult is an nsIAutoCompleteResult, aElement is
+ // nsIDOMHTMLInputElement
+
+ let form = LoginFormFactory.createFromField(aElement);
+ let isSecure = InsecurePasswordUtils.isFormSecure(form);
+ let isPasswordField = aElement.type == "password";
+
+ let completeSearch = (autoCompleteLookupPromise, { logins, messageManager }) => {
+ // If the search was canceled before we got our
+ // results, don't bother reporting them.
+ if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
+ return;
+ }
+
+ this._autoCompleteLookupPromise = null;
+ let results = new UserAutoCompleteResult(aSearchString, logins, {
+ messageManager,
+ isSecure,
+ isPasswordField,
+ });
+ aCallback.onSearchCompletion(results);
+ };
+
+ if (isPasswordField && aSearchString) {
+ // Return empty result on password fields with password already filled.
+ let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] });
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
+ return;
+ }
+
+ if (!this._remember) {
+ let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] });
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
+ return;
+ }
+
+ log.debug("AutoCompleteSearch invoked. Search is:", aSearchString);
+
+ let previousResult;
+ if (aPreviousResult) {
+ previousResult = { searchString: aPreviousResult.searchString,
+ logins: aPreviousResult.wrappedJSObject.logins };
+ } else {
+ previousResult = null;
+ }
+
+ let rect = BrowserUtils.getElementBoundingScreenRect(aElement);
+ let acLookupPromise = this._autoCompleteLookupPromise =
+ LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult,
+ aElement, rect);
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise))
+ .then(null, Cu.reportError);
+ },
+
+ stopSearch() {
+ this._autoCompleteLookupPromise = null;
+ },
+}; // end of LoginManager implementation
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);
diff --git a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
new file mode 100644
index 000000000..b66489234
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -0,0 +1,1701 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+const { PromptUtils } = Cu.import("resource://gre/modules/SharedPromptUtils.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+const LoginInfo =
+ Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo", "init");
+
+const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
+
+/**
+ * Constants for password prompt telemetry.
+ * Mirrored in mobile/android/components/LoginManagerPrompter.js */
+const PROMPT_DISPLAYED = 0;
+
+const PROMPT_ADD_OR_UPDATE = 1;
+const PROMPT_NOTNOW = 2;
+const PROMPT_NEVER = 3;
+
+/**
+ * Implements nsIPromptFactory
+ *
+ * Invoked by [toolkit/components/prompts/src/nsPrompter.js]
+ */
+function LoginManagerPromptFactory() {
+ Services.obs.addObserver(this, "quit-application-granted", true);
+ Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
+ Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true);
+}
+
+LoginManagerPromptFactory.prototype = {
+
+ classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ _asyncPrompts : {},
+ _asyncPromptInProgress : false,
+
+ observe : function (subject, topic, data) {
+ this.log("Observed: " + topic);
+ if (topic == "quit-application-granted") {
+ this._cancelPendingPrompts();
+ } else if (topic == "passwordmgr-crypto-login") {
+ // Start processing the deferred prompters.
+ this._doAsyncPrompt();
+ } else if (topic == "passwordmgr-crypto-loginCanceled") {
+ // User canceled a Master Password prompt, so go ahead and cancel
+ // all pending auth prompts to avoid nagging over and over.
+ this._cancelPendingPrompts();
+ }
+ },
+
+ getPrompt : function (aWindow, aIID) {
+ var prompt = new LoginManagerPrompter().QueryInterface(aIID);
+ prompt.init(aWindow, this);
+ return prompt;
+ },
+
+ _doAsyncPrompt : function() {
+ if (this._asyncPromptInProgress) {
+ this.log("_doAsyncPrompt bypassed, already in progress");
+ return;
+ }
+
+ // Find the first prompt key we have in the queue
+ var hashKey = null;
+ for (hashKey in this._asyncPrompts)
+ break;
+
+ if (!hashKey) {
+ this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
+ return;
+ }
+
+ // If login manger has logins for this host, defer prompting if we're
+ // already waiting on a master password entry.
+ var prompt = this._asyncPrompts[hashKey];
+ var prompter = prompt.prompter;
+ var [hostname, httpRealm] = prompter._getAuthTarget(prompt.channel, prompt.authInfo);
+ var hasLogins = (prompter._pwmgr.countLogins(hostname, null, httpRealm) > 0);
+ if (!hasLogins && LoginHelper.schemeUpgrades && hostname.startsWith("https://")) {
+ let httpHostname = hostname.replace(/^https:\/\//, "http://");
+ hasLogins = (prompter._pwmgr.countLogins(httpHostname, null, httpRealm) > 0);
+ }
+ if (hasLogins && prompter._pwmgr.uiBusy) {
+ this.log("_doAsyncPrompt:run bypassed, master password UI busy");
+ return;
+ }
+
+ // Allow only a limited number of authentication dialogs when they are all
+ // canceled by the user.
+ var cancelationCounter = (prompter._browser && prompter._browser.canceledAuthenticationPromptCounter) || { count: 0, id: 0 };
+ if (prompt.channel) {
+ var httpChannel = prompt.channel.QueryInterface(Ci.nsIHttpChannel);
+ if (httpChannel) {
+ var windowId = httpChannel.topLevelContentWindowId;
+ if (windowId != cancelationCounter.id) {
+ // window has been reloaded or navigated, reset the counter
+ cancelationCounter = { count: 0, id: windowId };
+ }
+ }
+ }
+
+ var self = this;
+
+ var runnable = {
+ cancel: false,
+ run : function() {
+ var ok = false;
+ if (!this.cancel) {
+ try {
+ self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'");
+ ok = prompter.promptAuth(prompt.channel,
+ prompt.level,
+ prompt.authInfo);
+ } catch (e) {
+ if (e instanceof Components.Exception &&
+ e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ self.log("_doAsyncPrompt:run bypassed, UI is not available in this context");
+ } else {
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "_doAsyncPrompt:run: " + e + "\n");
+ }
+ }
+
+ delete self._asyncPrompts[hashKey];
+ prompt.inProgress = false;
+ self._asyncPromptInProgress = false;
+
+ if (ok) {
+ cancelationCounter.count = 0;
+ } else {
+ cancelationCounter.count++;
+ }
+ if (prompter._browser) {
+ prompter._browser.canceledAuthenticationPromptCounter = cancelationCounter;
+ }
+ }
+
+ for (var consumer of prompt.consumers) {
+ if (!consumer.callback)
+ // Not having a callback means that consumer didn't provide it
+ // or canceled the notification
+ continue;
+
+ self.log("Calling back to " + consumer.callback + " ok=" + ok);
+ try {
+ if (ok) {
+ consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
+ } else {
+ consumer.callback.onAuthCancelled(consumer.context, !this.cancel);
+ }
+ } catch (e) { /* Throw away exceptions caused by callback */ }
+ }
+ self._doAsyncPrompt();
+ }
+ };
+
+ var cancelDialogLimit = Services.prefs.getIntPref("prompts.authentication_dialog_abuse_limit");
+
+ this.log("cancelationCounter =", cancelationCounter);
+ if (cancelDialogLimit && cancelationCounter.count >= cancelDialogLimit) {
+ this.log("Blocking auth dialog, due to exceeding dialog bloat limit");
+ delete this._asyncPrompts[hashKey];
+
+ // just make the runnable cancel all consumers
+ runnable.cancel = true;
+ } else {
+ this._asyncPromptInProgress = true;
+ prompt.inProgress = true;
+ }
+
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ this.log("_doAsyncPrompt:run dispatched");
+ },
+
+
+ _cancelPendingPrompts : function() {
+ this.log("Canceling all pending prompts...");
+ var asyncPrompts = this._asyncPrompts;
+ this.__proto__._asyncPrompts = {};
+
+ for (var hashKey in asyncPrompts) {
+ let prompt = asyncPrompts[hashKey];
+ // Watch out! If this prompt is currently prompting, let it handle
+ // notifying the callbacks of success/failure, since it's already
+ // asking the user for input. Reusing a callback can be crashy.
+ if (prompt.inProgress) {
+ this.log("skipping a prompt in progress");
+ continue;
+ }
+
+ for (var consumer of prompt.consumers) {
+ if (!consumer.callback)
+ continue;
+
+ this.log("Canceling async auth prompt callback " + consumer.callback);
+ try {
+ consumer.callback.onAuthCancelled(consumer.context, true);
+ } catch (e) { /* Just ignore exceptions from the callback */ }
+ }
+ }
+ },
+}; // end of LoginManagerPromptFactory implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerPromptFactory.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login PromptFactory");
+ return logger.log.bind(logger);
+});
+
+
+
+
+/* ==================== LoginManagerPrompter ==================== */
+
+
+
+
+/**
+ * Implements interfaces for prompting the user to enter/save/change auth info.
+ *
+ * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
+ *
+ * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
+ * (eg HTTP Authenticate, FTP login).
+ *
+ * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
+ * found in HTML forms.
+ */
+function LoginManagerPrompter() {}
+
+LoginManagerPrompter.prototype = {
+
+ classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt,
+ Ci.nsIAuthPrompt2,
+ Ci.nsILoginManagerPrompter]),
+
+ _factory : null,
+ _chromeWindow : null,
+ _browser : null,
+ _opener : null,
+
+ __pwmgr : null, // Password Manager service
+ get _pwmgr() {
+ if (!this.__pwmgr)
+ this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ return this.__pwmgr;
+ },
+
+ __promptService : null, // Prompt service for user interaction
+ get _promptService() {
+ if (!this.__promptService)
+ this.__promptService =
+ Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService2);
+ return this.__promptService;
+ },
+
+
+ __strBundle : null, // String bundle for L10N
+ get _strBundle() {
+ if (!this.__strBundle) {
+ var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ this.__strBundle = bunService.createBundle(
+ "chrome://passwordmgr/locale/passwordmgr.properties");
+ if (!this.__strBundle)
+ throw new Error("String bundle for Login Manager not present!");
+ }
+
+ return this.__strBundle;
+ },
+
+
+ __ellipsis : null,
+ get _ellipsis() {
+ if (!this.__ellipsis) {
+ this.__ellipsis = "\u2026";
+ try {
+ this.__ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+ }
+ return this.__ellipsis;
+ },
+
+
+ // Whether we are in private browsing mode
+ get _inPrivateBrowsing() {
+ if (this._chromeWindow) {
+ return PrivateBrowsingUtils.isWindowPrivate(this._chromeWindow);
+ }
+ // If we don't that we're in private browsing mode if the caller did
+ // not provide a window. The callers which really care about this
+ // will indeed pass down a window to us, and for those who don't,
+ // we can just assume that we don't want to save the entered login
+ // information.
+ this.log("We have no chromeWindow so assume we're in a private context");
+ return true;
+ },
+
+
+
+
+ /* ---------- nsIAuthPrompt prompts ---------- */
+
+
+ /**
+ * Wrapper around the prompt service prompt. Saving random fields here
+ * doesn't really make sense and therefore isn't implemented.
+ */
+ prompt : function (aDialogTitle, aText, aPasswordRealm,
+ aSavePassword, aDefaultText, aResult) {
+ if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER)
+ throw new Components.Exception("prompt only supports SAVE_PASSWORD_NEVER",
+ Cr.NS_ERROR_NOT_IMPLEMENTED);
+
+ this.log("===== prompt() called =====");
+
+ if (aDefaultText) {
+ aResult.value = aDefaultText;
+ }
+
+ return this._promptService.prompt(this._chromeWindow,
+ aDialogTitle, aText, aResult, null, {});
+ },
+
+
+ /**
+ * Looks up a username and password in the database. Will prompt the user
+ * with a dialog, even if a username and password are found.
+ */
+ promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm,
+ aSavePassword, aUsername, aPassword) {
+ this.log("===== promptUsernameAndPassword() called =====");
+
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
+ throw new Components.Exception("promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED);
+
+ var selectedLogin = null;
+ var checkBox = { value : false };
+ var checkBoxLabel = null;
+ var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm);
+
+ // If hostname is null, we can't save this login.
+ if (hostname) {
+ var canRememberLogin;
+ if (this._inPrivateBrowsing)
+ canRememberLogin = false;
+ else
+ canRememberLogin = (aSavePassword ==
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
+ this._pwmgr.getLoginSavingEnabled(hostname);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin)
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+
+ // Look for existing logins.
+ var foundLogins = this._pwmgr.findLogins({}, hostname, null,
+ realm);
+
+ // XXX Like the original code, we can't deal with multiple
+ // account selection. (bug 227632)
+ if (foundLogins.length > 0) {
+ selectedLogin = foundLogins[0];
+
+ // If the caller provided a username, try to use it. If they
+ // provided only a password, this will try to find a password-only
+ // login (or return null if none exists).
+ if (aUsername.value)
+ selectedLogin = this._repickSelectedLogin(foundLogins,
+ aUsername.value);
+
+ if (selectedLogin) {
+ checkBox.value = true;
+ aUsername.value = selectedLogin.username;
+ // If the caller provided a password, prefer it.
+ if (!aPassword.value)
+ aPassword.value = selectedLogin.password;
+ }
+ }
+ }
+
+ var ok = this._promptService.promptUsernameAndPassword(this._chromeWindow,
+ aDialogTitle, aText, aUsername, aPassword,
+ checkBoxLabel, checkBox);
+
+ if (!ok || !checkBox.value || !hostname)
+ return ok;
+
+ if (!aPassword.value) {
+ this.log("No password entered, so won't offer to save.");
+ return ok;
+ }
+
+ // XXX We can't prompt with multiple logins yet (bug 227632), so
+ // the entered login might correspond to an existing login
+ // other than the one we originally selected.
+ selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
+
+ // If we didn't find an existing login, or if the username
+ // changed, save as a new login.
+ let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLogin.init(hostname, null, realm,
+ aUsername.value, aPassword.value, "", "");
+ if (!selectedLogin) {
+ // add as new
+ this.log("New login seen for " + realm);
+ this._pwmgr.addLogin(newLogin);
+ } else if (aPassword.value != selectedLogin.password) {
+ // update password
+ this.log("Updating password for " + realm);
+ this._updateLogin(selectedLogin, newLogin);
+ } else {
+ this.log("Login unchanged, no further action needed.");
+ this._updateLogin(selectedLogin);
+ }
+
+ return ok;
+ },
+
+
+ /**
+ * If a password is found in the database for the password realm, it is
+ * returned straight away without displaying a dialog.
+ *
+ * If a password is not found in the database, the user will be prompted
+ * with a dialog with a text field and ok/cancel buttons. If the user
+ * allows it, then the password will be saved in the database.
+ */
+ promptPassword : function (aDialogTitle, aText, aPasswordRealm,
+ aSavePassword, aPassword) {
+ this.log("===== promptPassword called() =====");
+
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
+ throw new Components.Exception("promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED);
+
+ var checkBox = { value : false };
+ var checkBoxLabel = null;
+ var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm);
+
+ username = decodeURIComponent(username);
+
+ // If hostname is null, we can't save this login.
+ if (hostname && !this._inPrivateBrowsing) {
+ var canRememberLogin = (aSavePassword ==
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
+ this._pwmgr.getLoginSavingEnabled(hostname);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin)
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+
+ if (!aPassword.value) {
+ // Look for existing logins.
+ var foundLogins = this._pwmgr.findLogins({}, hostname, null,
+ realm);
+
+ // XXX Like the original code, we can't deal with multiple
+ // account selection (bug 227632). We can deal with finding the
+ // account based on the supplied username - but in this case we'll
+ // just return the first match.
+ for (var i = 0; i < foundLogins.length; ++i) {
+ if (foundLogins[i].username == username) {
+ aPassword.value = foundLogins[i].password;
+ // wallet returned straight away, so this mimics that code
+ return true;
+ }
+ }
+ }
+ }
+
+ var ok = this._promptService.promptPassword(this._chromeWindow, aDialogTitle,
+ aText, aPassword,
+ checkBoxLabel, checkBox);
+
+ if (ok && checkBox.value && hostname && aPassword.value) {
+ var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLogin.init(hostname, null, realm, username,
+ aPassword.value, "", "");
+
+ this.log("New login seen for " + realm);
+
+ this._pwmgr.addLogin(newLogin);
+ }
+
+ return ok;
+ },
+
+ /* ---------- nsIAuthPrompt helpers ---------- */
+
+
+ /**
+ * Given aRealmString, such as "http://user@example.com/foo", returns an
+ * array of:
+ * - the formatted hostname
+ * - the realm (hostname + path)
+ * - the username, if present
+ *
+ * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
+ * channels, e.g. "example.com:80 (httprealm)", null is returned for all
+ * arguments to let callers know the login can't be saved because we don't
+ * know whether it's http or https.
+ */
+ _getRealmInfo : function (aRealmString) {
+ var httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString))
+ return [null, null, null];
+
+ var uri = Services.io.newURI(aRealmString, null, null);
+ var pathname = "";
+
+ if (uri.path != "/")
+ pathname = uri.path;
+
+ var formattedHostname = this._getFormattedHostname(uri);
+
+ return [formattedHostname, formattedHostname + pathname, uri.username];
+ },
+
+ /* ---------- nsIAuthPrompt2 prompts ---------- */
+
+
+
+
+ /**
+ * Implementation of nsIAuthPrompt2.
+ *
+ * @param {nsIChannel} aChannel
+ * @param {int} aLevel
+ * @param {nsIAuthInformation} aAuthInfo
+ */
+ promptAuth : function (aChannel, aLevel, aAuthInfo) {
+ var selectedLogin = null;
+ var checkbox = { value : false };
+ var checkboxLabel = null;
+ var epicfail = false;
+ var canAutologin = false;
+ var notifyObj;
+ var foundLogins;
+
+ try {
+ this.log("===== promptAuth called =====");
+
+ // If the user submits a login but it fails, we need to remove the
+ // notification bar that was displayed. Conveniently, the user will
+ // be prompted for authentication again, which brings us here.
+ this._removeLoginNotifications();
+
+ var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
+
+ // Looks for existing logins to prefill the prompt with.
+ foundLogins = LoginHelper.searchLoginsWithObject({
+ hostname,
+ httpRealm,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+ this.log("found", foundLogins.length, "matching logins.");
+ let resolveBy = [
+ "scheme",
+ "timePasswordChanged",
+ ];
+ foundLogins = LoginHelper.dedupeLogins(foundLogins, ["username"], resolveBy, hostname);
+ this.log(foundLogins.length, "matching logins remain after deduping");
+
+ // XXX Can't select from multiple accounts yet. (bug 227632)
+ if (foundLogins.length > 0) {
+ selectedLogin = foundLogins[0];
+ this._SetAuthInfo(aAuthInfo, selectedLogin.username,
+ selectedLogin.password);
+
+ // Allow automatic proxy login
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
+ !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
+ Services.prefs.getBoolPref("signon.autologin.proxy") &&
+ !this._inPrivateBrowsing) {
+
+ this.log("Autologin enabled, skipping auth prompt.");
+ canAutologin = true;
+ }
+
+ checkbox.value = true;
+ }
+
+ var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname);
+ if (this._inPrivateBrowsing)
+ canRememberLogin = false;
+
+ // if checkboxLabel is null, the checkbox won't be shown at all.
+ notifyObj = this._getPopupNote() || this._getNotifyBox();
+ if (canRememberLogin && !notifyObj)
+ checkboxLabel = this._getLocalizedString("rememberPassword");
+ } catch (e) {
+ // Ignore any errors and display the prompt anyway.
+ epicfail = true;
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "Epic fail in promptAuth: " + e + "\n");
+ }
+
+ var ok = canAutologin;
+ if (!ok) {
+ if (this._chromeWindow)
+ PromptUtils.fireDialogEvent(this._chromeWindow, "DOMWillOpenModalDialog", this._browser);
+ ok = this._promptService.promptAuth(this._chromeWindow,
+ aChannel, aLevel, aAuthInfo,
+ checkboxLabel, checkbox);
+ }
+
+ // If there's a notification box, use it to allow the user to
+ // determine if the login should be saved. If there isn't a
+ // notification box, only save the login if the user set the
+ // checkbox to do so.
+ var rememberLogin = notifyObj ? canRememberLogin : checkbox.value;
+ if (!ok || !rememberLogin || epicfail)
+ return ok;
+
+ try {
+ var [username, password] = this._GetAuthInfo(aAuthInfo);
+
+ if (!password) {
+ this.log("No password entered, so won't offer to save.");
+ return ok;
+ }
+
+ // XXX We can't prompt with multiple logins yet (bug 227632), so
+ // the entered login might correspond to an existing login
+ // other than the one we originally selected.
+ selectedLogin = this._repickSelectedLogin(foundLogins, username);
+
+ // If we didn't find an existing login, or if the username
+ // changed, save as a new login.
+ let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLogin.init(hostname, null, httpRealm,
+ username, password, "", "");
+ if (!selectedLogin) {
+ this.log("New login seen for " + username +
+ " @ " + hostname + " (" + httpRealm + ")");
+
+ if (notifyObj)
+ this._showSaveLoginNotification(notifyObj, newLogin);
+ else
+ this._pwmgr.addLogin(newLogin);
+ } else if (password != selectedLogin.password) {
+ this.log("Updating password for " + username +
+ " @ " + hostname + " (" + httpRealm + ")");
+ if (notifyObj)
+ this._showChangeLoginNotification(notifyObj,
+ selectedLogin, newLogin);
+ else
+ this._updateLogin(selectedLogin, newLogin);
+ } else {
+ this.log("Login unchanged, no further action needed.");
+ this._updateLogin(selectedLogin);
+ }
+ } catch (e) {
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "Fail2 in promptAuth: " + e + "\n");
+ }
+
+ return ok;
+ },
+
+ asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) {
+ var cancelable = null;
+
+ try {
+ this.log("===== asyncPromptAuth called =====");
+
+ // If the user submits a login but it fails, we need to remove the
+ // notification bar that was displayed. Conveniently, the user will
+ // be prompted for authentication again, which brings us here.
+ this._removeLoginNotifications();
+
+ cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
+
+ var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
+
+ var hashKey = aLevel + "|" + hostname + "|" + httpRealm;
+ this.log("Async prompt key = " + hashKey);
+ var asyncPrompt = this._factory._asyncPrompts[hashKey];
+ if (asyncPrompt) {
+ this.log("Prompt bound to an existing one in the queue, callback = " + aCallback);
+ asyncPrompt.consumers.push(cancelable);
+ return cancelable;
+ }
+
+ this.log("Adding new prompt to the queue, callback = " + aCallback);
+ asyncPrompt = {
+ consumers: [cancelable],
+ channel: aChannel,
+ authInfo: aAuthInfo,
+ level: aLevel,
+ inProgress : false,
+ prompter: this
+ };
+
+ this._factory._asyncPrompts[hashKey] = asyncPrompt;
+ this._factory._doAsyncPrompt();
+ } catch (e) {
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "asyncPromptAuth: " + e + "\nFalling back to promptAuth\n");
+ // Fail the prompt operation to let the consumer fall back
+ // to synchronous promptAuth method
+ throw e;
+ }
+
+ return cancelable;
+ },
+
+
+
+
+ /* ---------- nsILoginManagerPrompter prompts ---------- */
+
+
+ init : function (aWindow = null, aFactory = null) {
+ if (!aWindow) {
+ // There may be no applicable window e.g. in a Sandbox or JSM.
+ this._chromeWindow = null;
+ this._browser = null;
+ } else if (aWindow instanceof Ci.nsIDOMChromeWindow) {
+ this._chromeWindow = aWindow;
+ // needs to be set explicitly using setBrowser
+ this._browser = null;
+ } else {
+ let {win, browser} = this._getChromeWindow(aWindow);
+ this._chromeWindow = win;
+ this._browser = browser;
+ }
+ this._opener = null;
+ this._factory = aFactory || null;
+
+ this.log("===== initialized =====");
+ },
+
+ set browser(aBrowser) {
+ this._browser = aBrowser;
+ },
+
+ set opener(aOpener) {
+ this._opener = aOpener;
+ },
+
+ promptToSavePassword : function (aLogin) {
+ this.log("promptToSavePassword");
+ var notifyObj = this._getPopupNote() || this._getNotifyBox();
+ if (notifyObj)
+ this._showSaveLoginNotification(notifyObj, aLogin);
+ else
+ this._showSaveLoginDialog(aLogin);
+ },
+
+ /**
+ * Displays a notification bar.
+ */
+ _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
+ var oldBar = aNotifyBox.getNotificationWithValue(aName);
+ const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
+
+ this.log("Adding new " + aName + " notification bar");
+ var newBar = aNotifyBox.appendNotification(
+ aText, aName, "",
+ priority, aButtons);
+
+ // The page we're going to hasn't loaded yet, so we want to persist
+ // across the first location change.
+ newBar.persistence++;
+
+ // Sites like Gmail perform a funky redirect dance before you end up
+ // at the post-authentication page. I don't see a good way to
+ // heuristically determine when to ignore such location changes, so
+ // we'll try ignoring location changes based on a time interval.
+ newBar.timeout = Date.now() + 20000; // 20 seconds
+
+ if (oldBar) {
+ this.log("(...and removing old " + aName + " notification bar)");
+ aNotifyBox.removeNotification(oldBar);
+ }
+ },
+
+ /**
+ * Displays the PopupNotifications.jsm doorhanger for password save or change.
+ *
+ * @param {nsILoginInfo} login
+ * Login to save or change. For changes, this login should contain the
+ * new password.
+ * @param {string} type
+ * This is "password-save" or "password-change" depending on the
+ * original notification type. This is used for telemetry and tests.
+ */
+ _showLoginCaptureDoorhanger(login, type) {
+ let { browser } = this._getNotifyWindow();
+
+ let saveMsgNames = {
+ prompt: login.username === "" ? "rememberLoginMsgNoUser"
+ : "rememberLoginMsg",
+ buttonLabel: "rememberLoginButtonText",
+ buttonAccessKey: "rememberLoginButtonAccessKey",
+ };
+
+ let changeMsgNames = {
+ prompt: login.username === "" ? "updateLoginMsgNoUser"
+ : "updateLoginMsg",
+ buttonLabel: "updateLoginButtonText",
+ buttonAccessKey: "updateLoginButtonAccessKey",
+ };
+
+ let initialMsgNames = type == "password-save" ? saveMsgNames
+ : changeMsgNames;
+
+ let brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
+ let brandShortName = brandBundle.GetStringFromName("brandShortName");
+ let promptMsg = type == "password-save" ? this._getLocalizedString(saveMsgNames.prompt, [brandShortName])
+ : this._getLocalizedString(changeMsgNames.prompt);
+
+ let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION"
+ : "PWMGR_PROMPT_UPDATE_ACTION";
+ let histogram = Services.telemetry.getHistogramById(histogramName);
+ histogram.add(PROMPT_DISPLAYED);
+
+ let chromeDoc = browser.ownerDocument;
+
+ let currentNotification;
+
+ let updateButtonStatus = (element) => {
+ let mainActionButton = chromeDoc.getAnonymousElementByAttribute(element.button, "anonid", "button");
+ // Disable the main button inside the menu-button if the password field is empty.
+ if (login.password.length == 0) {
+ mainActionButton.setAttribute("disabled", true);
+ chromeDoc.getElementById("password-notification-password")
+ .classList.add("popup-notification-invalid-input");
+ } else {
+ mainActionButton.removeAttribute("disabled");
+ chromeDoc.getElementById("password-notification-password")
+ .classList.remove("popup-notification-invalid-input");
+ }
+ };
+
+ let updateButtonLabel = () => {
+ let foundLogins = LoginHelper.searchLoginsWithObject({
+ formSubmitURL: login.formSubmitURL,
+ hostname: login.hostname,
+ httpRealm: login.httpRealm,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+
+ let logins = this._filterUpdatableLogins(login, foundLogins);
+ let msgNames = (logins.length == 0) ? saveMsgNames : changeMsgNames;
+
+ // Update the label based on whether this will be a new login or not.
+ let label = this._getLocalizedString(msgNames.buttonLabel);
+ let accessKey = this._getLocalizedString(msgNames.buttonAccessKey);
+
+ // Update the labels for the next time the panel is opened.
+ currentNotification.mainAction.label = label;
+ currentNotification.mainAction.accessKey = accessKey;
+
+ // Update the labels in real time if the notification is displayed.
+ let element = [...currentNotification.owner.panel.childNodes]
+ .find(n => n.notification == currentNotification);
+ if (element) {
+ element.setAttribute("buttonlabel", label);
+ element.setAttribute("buttonaccesskey", accessKey);
+ updateButtonStatus(element);
+ }
+ };
+
+ let writeDataToUI = () => {
+ // setAttribute is used since the <textbox> binding may not be attached yet.
+ chromeDoc.getElementById("password-notification-username")
+ .setAttribute("placeholder", usernamePlaceholder);
+ chromeDoc.getElementById("password-notification-username")
+ .setAttribute("value", login.username);
+
+ let toggleCheckbox = chromeDoc.getElementById("password-notification-visibilityToggle");
+ toggleCheckbox.removeAttribute("checked");
+ let passwordField = chromeDoc.getElementById("password-notification-password");
+ // Ensure the type is reset so the field is masked.
+ passwordField.setAttribute("type", "password");
+ passwordField.setAttribute("value", login.password);
+ updateButtonLabel();
+ };
+
+ let readDataFromUI = () => {
+ login.username =
+ chromeDoc.getElementById("password-notification-username").value;
+ login.password =
+ chromeDoc.getElementById("password-notification-password").value;
+ };
+
+ let onInput = () => {
+ readDataFromUI();
+ updateButtonLabel();
+ };
+
+ let onVisibilityToggle = (commandEvent) => {
+ let passwordField = chromeDoc.getElementById("password-notification-password");
+ // Gets the caret position before changing the type of the textbox
+ let selectionStart = passwordField.selectionStart;
+ let selectionEnd = passwordField.selectionEnd;
+ passwordField.setAttribute("type", commandEvent.target.checked ? "" : "password");
+ if (!passwordField.hasAttribute("focused")) {
+ return;
+ }
+ passwordField.selectionStart = selectionStart;
+ passwordField.selectionEnd = selectionEnd;
+ };
+
+ let persistData = () => {
+ let foundLogins = LoginHelper.searchLoginsWithObject({
+ formSubmitURL: login.formSubmitURL,
+ hostname: login.hostname,
+ httpRealm: login.httpRealm,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+
+ let logins = this._filterUpdatableLogins(login, foundLogins);
+
+ if (logins.length == 0) {
+ // The original login we have been provided with might have its own
+ // metadata, but we don't want it propagated to the newly created one.
+ Services.logins.addLogin(new LoginInfo(login.hostname,
+ login.formSubmitURL,
+ login.httpRealm,
+ login.username,
+ login.password,
+ login.usernameField,
+ login.passwordField));
+ } else if (logins.length == 1) {
+ if (logins[0].password == login.password &&
+ logins[0].username == login.username) {
+ // We only want to touch the login's use count and last used time.
+ this._updateLogin(logins[0]);
+ } else {
+ this._updateLogin(logins[0], login);
+ }
+ } else {
+ Cu.reportError("Unexpected match of multiple logins.");
+ }
+ };
+
+ // The main action is the "Remember" or "Update" button.
+ let mainAction = {
+ label: this._getLocalizedString(initialMsgNames.buttonLabel),
+ accessKey: this._getLocalizedString(initialMsgNames.buttonAccessKey),
+ callback: () => {
+ histogram.add(PROMPT_ADD_OR_UPDATE);
+ if (histogramName == "PWMGR_PROMPT_REMEMBER_ACTION") {
+ Services.obs.notifyObservers(null, 'LoginStats:NewSavedPassword', null);
+ }
+ readDataFromUI();
+ persistData();
+ browser.focus();
+ }
+ };
+
+ // Include a "Never for this site" button when saving a new password.
+ let secondaryActions = type == "password-save" ? [{
+ label: this._getLocalizedString("notifyBarNeverRememberButtonText"),
+ accessKey: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"),
+ callback: () => {
+ histogram.add(PROMPT_NEVER);
+ Services.logins.setLoginSavingEnabled(login.hostname, false);
+ browser.focus();
+ }
+ }] : null;
+
+ let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder");
+ let togglePasswordLabel = this._getLocalizedString("togglePasswordLabel");
+ let togglePasswordAccessKey = this._getLocalizedString("togglePasswordAccessKey");
+
+ this._getPopupNote().show(
+ browser,
+ "password",
+ promptMsg,
+ "password-notification-icon",
+ mainAction,
+ secondaryActions,
+ {
+ timeout: Date.now() + 10000,
+ displayURI: Services.io.newURI(login.hostname, null, null),
+ persistWhileVisible: true,
+ passwordNotificationType: type,
+ eventCallback: function (topic) {
+ switch (topic) {
+ case "showing":
+ currentNotification = this;
+ chromeDoc.getElementById("password-notification-password")
+ .removeAttribute("focused");
+ chromeDoc.getElementById("password-notification-username")
+ .removeAttribute("focused");
+ chromeDoc.getElementById("password-notification-username")
+ .addEventListener("input", onInput);
+ chromeDoc.getElementById("password-notification-password")
+ .addEventListener("input", onInput);
+ let toggleBtn = chromeDoc.getElementById("password-notification-visibilityToggle");
+
+ if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) {
+ toggleBtn.addEventListener("command", onVisibilityToggle);
+ toggleBtn.setAttribute("label", togglePasswordLabel);
+ toggleBtn.setAttribute("accesskey", togglePasswordAccessKey);
+ toggleBtn.setAttribute("hidden", LoginHelper.isMasterPasswordSet());
+ }
+ if (this.wasDismissed) {
+ chromeDoc.getElementById("password-notification-visibilityToggle")
+ .setAttribute("hidden", true);
+ }
+ break;
+ case "shown":
+ writeDataToUI();
+ break;
+ case "dismissed":
+ this.wasDismissed = true;
+ readDataFromUI();
+ // Fall through.
+ case "removed":
+ currentNotification = null;
+ chromeDoc.getElementById("password-notification-username")
+ .removeEventListener("input", onInput);
+ chromeDoc.getElementById("password-notification-password")
+ .removeEventListener("input", onInput);
+ chromeDoc.getElementById("password-notification-visibilityToggle")
+ .removeEventListener("command", onVisibilityToggle);
+ break;
+ }
+ return false;
+ },
+ }
+ );
+ },
+
+ /**
+ * Displays a notification bar or a popup notification, to allow the user
+ * to save the specified login. This allows the user to see the results of
+ * their login, and only save a login which they know worked.
+ *
+ * @param aNotifyObj
+ * A notification box or a popup notification.
+ * @param aLogin
+ * The login captured from the form.
+ */
+ _showSaveLoginNotification : function (aNotifyObj, aLogin) {
+ // Ugh. We can't use the strings from the popup window, because they
+ // have the access key marked in the string (eg "Mo&zilla"), along
+ // with some weird rules for handling access keys that do not occur
+ // in the string, for L10N. See commonDialog.js's setLabelForNode().
+ var neverButtonText =
+ this._getLocalizedString("notifyBarNeverRememberButtonText");
+ var neverButtonAccessKey =
+ this._getLocalizedString("notifyBarNeverRememberButtonAccessKey");
+ var rememberButtonText =
+ this._getLocalizedString("notifyBarRememberPasswordButtonText");
+ var rememberButtonAccessKey =
+ this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
+
+ var displayHost = this._getShortDisplayHost(aLogin.hostname);
+ var notificationText = this._getLocalizedString(
+ "rememberPasswordMsgNoUsername",
+ [displayHost]);
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var pwmgr = this._pwmgr;
+
+ // Notification is a PopupNotification
+ if (aNotifyObj == this._getPopupNote()) {
+ this._showLoginCaptureDoorhanger(aLogin, "password-save");
+ } else {
+ var notNowButtonText =
+ this._getLocalizedString("notifyBarNotNowButtonText");
+ var notNowButtonAccessKey =
+ this._getLocalizedString("notifyBarNotNowButtonAccessKey");
+ var buttons = [
+ // "Remember" button
+ {
+ label: rememberButtonText,
+ accessKey: rememberButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ pwmgr.addLogin(aLogin);
+ }
+ },
+
+ // "Never for this site" button
+ {
+ label: neverButtonText,
+ accessKey: neverButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ }
+ },
+
+ // "Not now" button
+ {
+ label: notNowButtonText,
+ accessKey: notNowButtonAccessKey,
+ popup: null,
+ callback: function() { /* NOP */ }
+ }
+ ];
+
+ this._showLoginNotification(aNotifyObj, "password-save",
+ notificationText, buttons);
+ }
+
+ Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
+ },
+
+ _removeLoginNotifications : function () {
+ var popupNote = this._getPopupNote();
+ if (popupNote)
+ popupNote = popupNote.getNotification("password");
+ if (popupNote)
+ popupNote.remove();
+
+ var notifyBox = this._getNotifyBox();
+ if (notifyBox) {
+ var oldBar = notifyBox.getNotificationWithValue("password-save");
+ if (oldBar) {
+ this.log("Removing save-password notification bar.");
+ notifyBox.removeNotification(oldBar);
+ }
+
+ oldBar = notifyBox.getNotificationWithValue("password-change");
+ if (oldBar) {
+ this.log("Removing change-password notification bar.");
+ notifyBox.removeNotification(oldBar);
+ }
+ }
+ },
+
+
+ /**
+ * Called when we detect a new login in a form submission,
+ * asks the user what to do.
+ */
+ _showSaveLoginDialog : function (aLogin) {
+ const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
+
+ var displayHost = this._getShortDisplayHost(aLogin.hostname);
+
+ var dialogText;
+ if (aLogin.username) {
+ var displayUser = this._sanitizeUsername(aLogin.username);
+ dialogText = this._getLocalizedString(
+ "rememberPasswordMsg",
+ [displayUser, displayHost]);
+ } else {
+ dialogText = this._getLocalizedString(
+ "rememberPasswordMsgNoUsername",
+ [displayHost]);
+
+ }
+ var dialogTitle = this._getLocalizedString(
+ "savePasswordTitle");
+ var neverButtonText = this._getLocalizedString(
+ "neverForSiteButtonText");
+ var rememberButtonText = this._getLocalizedString(
+ "rememberButtonText");
+ var notNowButtonText = this._getLocalizedString(
+ "notNowButtonText");
+
+ this.log("Prompting user to save/ignore login");
+ var userChoice = this._promptService.confirmEx(this._chromeWindow,
+ dialogTitle, dialogText,
+ buttonFlags, rememberButtonText,
+ notNowButtonText, neverButtonText,
+ null, {});
+ // Returns:
+ // 0 - Save the login
+ // 1 - Ignore the login this time
+ // 2 - Never save logins for this site
+ if (userChoice == 2) {
+ this.log("Disabling " + aLogin.hostname + " logins by request.");
+ this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ } else if (userChoice == 0) {
+ this.log("Saving login for " + aLogin.hostname);
+ this._pwmgr.addLogin(aLogin);
+ } else {
+ // userChoice == 1 --> just ignore the login.
+ this.log("Ignoring login.");
+ }
+
+ Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
+ },
+
+
+ /**
+ * Called when we think we detect a password or username change for
+ * an existing login, when the form being submitted contains multiple
+ * password fields.
+ *
+ * @param {nsILoginInfo} aOldLogin
+ * The old login we may want to update.
+ * @param {nsILoginInfo} aNewLogin
+ * The new login from the page form.
+ */
+ promptToChangePassword(aOldLogin, aNewLogin) {
+ this.log("promptToChangePassword");
+ let notifyObj = this._getPopupNote() || this._getNotifyBox();
+
+ if (notifyObj) {
+ this._showChangeLoginNotification(notifyObj, aOldLogin,
+ aNewLogin);
+ } else {
+ this._showChangeLoginDialog(aOldLogin, aNewLogin);
+ }
+ },
+
+ /**
+ * Shows the Change Password notification bar or popup notification.
+ *
+ * @param aNotifyObj
+ * A notification box or a popup notification.
+ *
+ * @param aOldLogin
+ * The stored login we want to update.
+ *
+ * @param aNewLogin
+ * The login object with the changes we want to make.
+ */
+ _showChangeLoginNotification(aNotifyObj, aOldLogin, aNewLogin) {
+ var changeButtonText =
+ this._getLocalizedString("notifyBarUpdateButtonText");
+ var changeButtonAccessKey =
+ this._getLocalizedString("notifyBarUpdateButtonAccessKey");
+
+ // We reuse the existing message, even if it expects a username, until we
+ // switch to the final terminology in bug 1144856.
+ var displayHost = this._getShortDisplayHost(aOldLogin.hostname);
+ var notificationText = this._getLocalizedString("updatePasswordMsg",
+ [displayHost]);
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var self = this;
+
+ // Notification is a PopupNotification
+ if (aNotifyObj == this._getPopupNote()) {
+ aOldLogin.hostname = aNewLogin.hostname;
+ aOldLogin.formSubmitURL = aNewLogin.formSubmitURL;
+ aOldLogin.password = aNewLogin.password;
+ aOldLogin.username = aNewLogin.username;
+ this._showLoginCaptureDoorhanger(aOldLogin, "password-change");
+ } else {
+ var dontChangeButtonText =
+ this._getLocalizedString("notifyBarDontChangeButtonText");
+ var dontChangeButtonAccessKey =
+ this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
+ var buttons = [
+ // "Yes" button
+ {
+ label: changeButtonText,
+ accessKey: changeButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ self._updateLogin(aOldLogin, aNewLogin);
+ }
+ },
+
+ // "No" button
+ {
+ label: dontChangeButtonText,
+ accessKey: dontChangeButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ // do nothing
+ }
+ }
+ ];
+
+ this._showLoginNotification(aNotifyObj, "password-change",
+ notificationText, buttons);
+ }
+
+ let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+ Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
+ },
+
+
+ /**
+ * Shows the Change Password dialog.
+ */
+ _showChangeLoginDialog(aOldLogin, aNewLogin) {
+ const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
+
+ var dialogText;
+ if (aOldLogin.username)
+ dialogText = this._getLocalizedString(
+ "updatePasswordMsg",
+ [aOldLogin.username]);
+ else
+ dialogText = this._getLocalizedString(
+ "updatePasswordMsgNoUser");
+
+ var dialogTitle = this._getLocalizedString(
+ "passwordChangeTitle");
+
+ // returns 0 for yes, 1 for no.
+ var ok = !this._promptService.confirmEx(this._chromeWindow,
+ dialogTitle, dialogText, buttonFlags,
+ null, null, null,
+ null, {});
+ if (ok) {
+ this.log("Updating password for user " + aOldLogin.username);
+ this._updateLogin(aOldLogin, aNewLogin);
+ }
+
+ let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+ Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
+ },
+
+
+ /**
+ * Called when we detect a password change in a form submission, but we
+ * don't know which existing login (username) it's for. Asks the user
+ * to select a username and confirm the password change.
+ *
+ * Note: The caller doesn't know the username for aNewLogin, so this
+ * function fills in .username and .usernameField with the values
+ * from the login selected by the user.
+ *
+ * Note; XPCOM stupidity: |count| is just |logins.length|.
+ */
+ promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
+ this.log("promptToChangePasswordWithUsernames with count:", count);
+
+ var usernames = logins.map(l => l.username);
+ var dialogText = this._getLocalizedString("userSelectText");
+ var dialogTitle = this._getLocalizedString("passwordChangeTitle");
+ var selectedIndex = { value: null };
+
+ // If user selects ok, outparam.value is set to the index
+ // of the selected username.
+ var ok = this._promptService.select(this._chromeWindow,
+ dialogTitle, dialogText,
+ usernames.length, usernames,
+ selectedIndex);
+ if (ok) {
+ // Now that we know which login to use, modify its password.
+ var selectedLogin = logins[selectedIndex.value];
+ this.log("Updating password for user " + selectedLogin.username);
+ var newLoginWithUsername = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLoginWithUsername.init(aNewLogin.hostname,
+ aNewLogin.formSubmitURL, aNewLogin.httpRealm,
+ selectedLogin.username, aNewLogin.password,
+ selectedLogin.userNameField, aNewLogin.passwordField);
+ this._updateLogin(selectedLogin, newLoginWithUsername);
+ }
+ },
+
+
+
+
+ /* ---------- Internal Methods ---------- */
+
+
+
+
+ _updateLogin(login, aNewLogin = null) {
+ var now = Date.now();
+ var propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ if (aNewLogin) {
+ propBag.setProperty("formSubmitURL", aNewLogin.formSubmitURL);
+ propBag.setProperty("hostname", aNewLogin.hostname);
+ propBag.setProperty("password", aNewLogin.password);
+ propBag.setProperty("username", aNewLogin.username);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+ this._pwmgr.modifyLogin(login, propBag);
+ },
+
+ /**
+ * Given a content DOM window, returns the chrome window and browser it's in.
+ */
+ _getChromeWindow: function (aWindow) {
+ let windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ let browser = win.gBrowser.getBrowserForContentWindow(aWindow);
+ if (browser) {
+ return { win, browser };
+ }
+ }
+ return null;
+ },
+
+ _getNotifyWindow: function () {
+ // Some sites pop up a temporary login window, which disappears
+ // upon submission of credentials. We want to put the notification
+ // bar in the opener window if this seems to be happening.
+ if (this._opener) {
+ let chromeDoc = this._chromeWindow.document.documentElement;
+
+ // Check to see if the current window was opened with chrome
+ // disabled, and if so use the opener window. But if the window
+ // has been used to visit other pages (ie, has a history),
+ // assume it'll stick around and *don't* use the opener.
+ if (chromeDoc.getAttribute("chromehidden") && !this._browser.canGoBack) {
+ this.log("Using opener window for notification bar.");
+ return this._getChromeWindow(this._opener);
+ }
+ }
+
+ return { win: this._chromeWindow, browser: this._browser };
+ },
+
+ /**
+ * Returns the popup notification to this prompter,
+ * or null if there isn't one available.
+ */
+ _getPopupNote : function () {
+ let popupNote = null;
+
+ try {
+ let { win: notifyWin } = this._getNotifyWindow();
+
+ // .wrappedJSObject needed here -- see bug 422974 comment 5.
+ popupNote = notifyWin.wrappedJSObject.PopupNotifications;
+ } catch (e) {
+ this.log("Popup notifications not available on window");
+ }
+
+ return popupNote;
+ },
+
+
+ /**
+ * Returns the notification box to this prompter, or null if there isn't
+ * a notification box available.
+ */
+ _getNotifyBox : function () {
+ let notifyBox = null;
+
+ try {
+ let { win: notifyWin } = this._getNotifyWindow();
+
+ // .wrappedJSObject needed here -- see bug 422974 comment 5.
+ notifyBox = notifyWin.wrappedJSObject.getNotificationBox(notifyWin);
+ } catch (e) {
+ this.log("Notification bars not available on window");
+ }
+
+ return notifyBox;
+ },
+
+
+ /**
+ * The user might enter a login that isn't the one we prefilled, but
+ * is the same as some other existing login. So, pick a login with a
+ * matching username, or return null.
+ */
+ _repickSelectedLogin : function (foundLogins, username) {
+ for (var i = 0; i < foundLogins.length; i++)
+ if (foundLogins[i].username == username)
+ return foundLogins[i];
+ return null;
+ },
+
+
+ /**
+ * Can be called as:
+ * _getLocalizedString("key1");
+ * _getLocalizedString("key2", ["arg1"]);
+ * _getLocalizedString("key3", ["arg1", "arg2"]);
+ * (etc)
+ *
+ * Returns the localized string for the specified key,
+ * formatted if required.
+ *
+ */
+ _getLocalizedString : function (key, formatArgs) {
+ if (formatArgs)
+ return this._strBundle.formatStringFromName(
+ key, formatArgs, formatArgs.length);
+ return this._strBundle.GetStringFromName(key);
+ },
+
+
+ /**
+ * Sanitizes the specified username, by stripping quotes and truncating if
+ * it's too long. This helps prevent an evil site from messing with the
+ * "save password?" prompt too much.
+ */
+ _sanitizeUsername : function (username) {
+ if (username.length > 30) {
+ username = username.substring(0, 30);
+ username += this._ellipsis;
+ }
+ return username.replace(/['"]/g, "");
+ },
+
+
+ /**
+ * The aURI parameter may either be a string uri, or an nsIURI instance.
+ *
+ * Returns the hostname to use in a nsILoginInfo object (for example,
+ * "http://example.com").
+ */
+ _getFormattedHostname : function (aURI) {
+ let uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI, null, null);
+ }
+
+ return uri.scheme + "://" + uri.hostPort;
+ },
+
+
+ /**
+ * Converts a login's hostname field (a URL) to a short string for
+ * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
+ * "ftp://www.site.co.uk" --> "site.co.uk".
+ */
+ _getShortDisplayHost: function (aURIString) {
+ var displayHost;
+
+ var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
+ getService(Ci.nsIEffectiveTLDService);
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+ try {
+ var uri = Services.io.newURI(aURIString, null, null);
+ var baseDomain = eTLDService.getBaseDomain(uri);
+ displayHost = idnService.convertToDisplayIDN(baseDomain, {});
+ } catch (e) {
+ this.log("_getShortDisplayHost couldn't process " + aURIString);
+ }
+
+ if (!displayHost)
+ displayHost = aURIString;
+
+ return displayHost;
+ },
+
+
+ /**
+ * Returns the hostname and realm for which authentication is being
+ * requested, in the format expected to be used with nsILoginInfo.
+ */
+ _getAuthTarget : function (aChannel, aAuthInfo) {
+ var hostname, realm;
+
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ this.log("getAuthTarget is for proxy auth");
+ if (!(aChannel instanceof Ci.nsIProxiedChannel))
+ throw new Error("proxy auth needs nsIProxiedChannel");
+
+ var info = aChannel.proxyInfo;
+ if (!info)
+ throw new Error("proxy auth needs nsIProxyInfo");
+
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+ hostname = "moz-proxy://" +
+ idnService.convertUTF8toACE(info.host) +
+ ":" + info.port;
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ }
+
+ hostname = this._getFormattedHostname(aChannel.URI);
+
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ },
+
+
+ /**
+ * Returns [username, password] as extracted from aAuthInfo (which
+ * holds this info after having prompted the user).
+ *
+ * If the authentication was for a Windows domain, we'll prepend the
+ * return username with the domain. (eg, "domain\user")
+ */
+ _GetAuthInfo : function (aAuthInfo) {
+ var username, password;
+
+ var flags = aAuthInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
+ username = aAuthInfo.domain + "\\" + aAuthInfo.username;
+ else
+ username = aAuthInfo.username;
+
+ password = aAuthInfo.password;
+
+ return [username, password];
+ },
+
+
+ /**
+ * Given a username (possibly in DOMAIN\user form) and password, parses the
+ * domain out of the username if necessary and sets domain, username and
+ * password on the auth information object.
+ */
+ _SetAuthInfo : function (aAuthInfo, username, password) {
+ var flags = aAuthInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ var idx = username.indexOf("\\");
+ if (idx == -1) {
+ aAuthInfo.username = username;
+ } else {
+ aAuthInfo.domain = username.substring(0, idx);
+ aAuthInfo.username = username.substring(idx + 1);
+ }
+ } else {
+ aAuthInfo.username = username;
+ }
+ aAuthInfo.password = password;
+ },
+
+ _newAsyncPromptConsumer : function(aCallback, aContext) {
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ callback: aCallback,
+ context: aContext,
+ cancel: function() {
+ this.callback.onAuthCancelled(this.context, false);
+ this.callback = null;
+ this.context = null;
+ }
+ };
+ },
+
+ /**
+ * This function looks for existing logins that can be updated
+ * to match a submitted login, instead of creating a new one.
+ *
+ * Given a login and a loginList, it filters the login list
+ * to find every login with either the same username as aLogin
+ * or with the same password as aLogin and an empty username
+ * so the user can add a username.
+ *
+ * @param {nsILoginInfo} aLogin
+ * login to use as filter.
+ * @param {nsILoginInfo[]} aLoginList
+ * Array of logins to filter.
+ * @returns {nsILoginInfo[]} the filtered array of logins.
+ */
+ _filterUpdatableLogins(aLogin, aLoginList) {
+ return aLoginList.filter(l => l.username == aLogin.username ||
+ (l.password == aLogin.password &&
+ !l.username));
+ },
+
+}; // end of LoginManagerPrompter implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerPrompter.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("LoginManagerPrompter");
+ return logger.log.bind(logger);
+});
+
+var component = [LoginManagerPromptFactory, LoginManagerPrompter];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/passwordmgr/passwordmgr.manifest b/toolkit/components/passwordmgr/passwordmgr.manifest
new file mode 100644
index 000000000..72e9ccffb
--- /dev/null
+++ b/toolkit/components/passwordmgr/passwordmgr.manifest
@@ -0,0 +1,17 @@
+component {cb9e0de8-3598-4ed7-857b-827f011ad5d8} nsLoginManager.js
+contract @mozilla.org/login-manager;1 {cb9e0de8-3598-4ed7-857b-827f011ad5d8}
+component {749e62f4-60ae-4569-a8a2-de78b649660e} nsLoginManagerPrompter.js
+contract @mozilla.org/passwordmanager/authpromptfactory;1 {749e62f4-60ae-4569-a8a2-de78b649660e}
+component {8aa66d77-1bbb-45a6-991e-b8f47751c291} nsLoginManagerPrompter.js
+contract @mozilla.org/login-manager/prompter;1 {8aa66d77-1bbb-45a6-991e-b8f47751c291}
+component {0f2f347c-1e4f-40cc-8efd-792dea70a85e} nsLoginInfo.js
+contract @mozilla.org/login-manager/loginInfo;1 {0f2f347c-1e4f-40cc-8efd-792dea70a85e}
+#ifdef ANDROID
+component {8c2023b9-175c-477e-9761-44ae7b549756} storage-mozStorage.js
+contract @mozilla.org/login-manager/storage/mozStorage;1 {8c2023b9-175c-477e-9761-44ae7b549756}
+#else
+component {c00c432d-a0c9-46d7-bef6-9c45b4d07341} storage-json.js
+contract @mozilla.org/login-manager/storage/json;1 {c00c432d-a0c9-46d7-bef6-9c45b4d07341}
+#endif
+component {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} crypto-SDR.js
+contract @mozilla.org/login-manager/crypto/SDR;1 {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} \ No newline at end of file
diff --git a/toolkit/components/passwordmgr/storage-json.js b/toolkit/components/passwordmgr/storage-json.js
new file mode 100644
index 000000000..20834d45b
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-json.js
@@ -0,0 +1,514 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * nsILoginManagerStorage implementation for the JSON back-end.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
+ "resource://gre/modules/LoginImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
+ "resource://gre/modules/LoginStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+this.LoginManagerStorage_json = function () {};
+
+this.LoginManagerStorage_json.prototype = {
+ classID: Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
+
+ __crypto: null, // nsILoginManagerCrypto service
+ get _crypto() {
+ if (!this.__crypto)
+ this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
+ getService(Ci.nsILoginManagerCrypto);
+ return this.__crypto;
+ },
+
+ initialize() {
+ try {
+ // Force initialization of the crypto module.
+ // See bug 717490 comment 17.
+ this._crypto;
+
+ // Set the reference to LoginStore synchronously.
+ let jsonPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "logins.json");
+ this._store = new LoginStore(jsonPath);
+
+ return Task.spawn(function* () {
+ // Load the data asynchronously.
+ this.log("Opening database at", this._store.path);
+ yield this._store.load();
+
+ // The import from previous versions operates the first time
+ // that this built-in storage back-end is used. This may be
+ // later than expected, in case add-ons have registered an
+ // alternate storage that disabled the default one.
+ try {
+ if (Services.prefs.getBoolPref("signon.importedFromSqlite")) {
+ return;
+ }
+ } catch (ex) {
+ // If the preference does not exist, we need to import.
+ }
+
+ // Import only happens asynchronously.
+ let sqlitePath = OS.Path.join(OS.Constants.Path.profileDir,
+ "signons.sqlite");
+ if (yield OS.File.exists(sqlitePath)) {
+ let loginImport = new LoginImport(this._store, sqlitePath);
+ // Failures during import, for example due to a corrupt
+ // file or a schema version that is too old, will not
+ // prevent us from marking the operation as completed.
+ // At the next startup, we will not try the import again.
+ yield loginImport.import().catch(Cu.reportError);
+ this._store.saveSoon();
+ }
+
+ // We won't attempt import again on next startup.
+ Services.prefs.setBoolPref("signon.importedFromSqlite", true);
+ }.bind(this)).catch(Cu.reportError);
+ } catch (e) {
+ this.log("Initialization failed:", e);
+ throw new Error("Initialization failed");
+ }
+ },
+
+ /**
+ * Internal method used by regression tests only. It is called before
+ * replacing this storage module with a new instance.
+ */
+ terminate() {
+ this._store._saver.disarm();
+ return this._store._save();
+ },
+
+ addLogin(login) {
+ this._store.ensureDataReady();
+
+ // Throws if there are bogus values.
+ LoginHelper.checkLoginValues(login);
+
+ let [encUsername, encPassword, encType] = this._encryptLogin(login);
+
+ // Clone the login, so we don't modify the caller's object.
+ let loginClone = login.clone();
+
+ // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
+ loginClone.QueryInterface(Ci.nsILoginMetaInfo);
+ if (loginClone.guid) {
+ if (!this._isGuidUnique(loginClone.guid))
+ throw new Error("specified GUID already exists");
+ } else {
+ loginClone.guid = gUUIDGenerator.generateUUID().toString();
+ }
+
+ // Set timestamps
+ let currentTime = Date.now();
+ if (!loginClone.timeCreated)
+ loginClone.timeCreated = currentTime;
+ if (!loginClone.timeLastUsed)
+ loginClone.timeLastUsed = currentTime;
+ if (!loginClone.timePasswordChanged)
+ loginClone.timePasswordChanged = currentTime;
+ if (!loginClone.timesUsed)
+ loginClone.timesUsed = 1;
+
+ this._store.data.logins.push({
+ id: this._store.data.nextId++,
+ hostname: loginClone.hostname,
+ httpRealm: loginClone.httpRealm,
+ formSubmitURL: loginClone.formSubmitURL,
+ usernameField: loginClone.usernameField,
+ passwordField: loginClone.passwordField,
+ encryptedUsername: encUsername,
+ encryptedPassword: encPassword,
+ guid: loginClone.guid,
+ encType: encType,
+ timeCreated: loginClone.timeCreated,
+ timeLastUsed: loginClone.timeLastUsed,
+ timePasswordChanged: loginClone.timePasswordChanged,
+ timesUsed: loginClone.timesUsed
+ });
+ this._store.saveSoon();
+
+ // Send a notification that a login was added.
+ LoginHelper.notifyStorageChanged("addLogin", loginClone);
+ return loginClone;
+ },
+
+ removeLogin(login) {
+ this._store.ensureDataReady();
+
+ let [idToDelete, storedLogin] = this._getIdForLogin(login);
+ if (!idToDelete)
+ throw new Error("No matching logins");
+
+ let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
+ if (foundIndex != -1) {
+ this._store.data.logins.splice(foundIndex, 1);
+ this._store.saveSoon();
+ }
+
+ LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
+ },
+
+ modifyLogin(oldLogin, newLoginData) {
+ this._store.ensureDataReady();
+
+ let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
+ if (!idToModify)
+ throw new Error("No matching logins");
+
+ let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
+
+ // Check if the new GUID is duplicate.
+ if (newLogin.guid != oldStoredLogin.guid &&
+ !this._isGuidUnique(newLogin.guid)) {
+ throw new Error("specified GUID already exists");
+ }
+
+ // Look for an existing entry in case key properties changed.
+ if (!newLogin.matches(oldLogin, true)) {
+ let logins = this.findLogins({}, newLogin.hostname,
+ newLogin.formSubmitURL,
+ newLogin.httpRealm);
+
+ if (logins.some(login => newLogin.matches(login, true)))
+ throw new Error("This login already exists.");
+ }
+
+ // Get the encrypted value of the username and password.
+ let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
+
+ for (let loginItem of this._store.data.logins) {
+ if (loginItem.id == idToModify) {
+ loginItem.hostname = newLogin.hostname;
+ loginItem.httpRealm = newLogin.httpRealm;
+ loginItem.formSubmitURL = newLogin.formSubmitURL;
+ loginItem.usernameField = newLogin.usernameField;
+ loginItem.passwordField = newLogin.passwordField;
+ loginItem.encryptedUsername = encUsername;
+ loginItem.encryptedPassword = encPassword;
+ loginItem.guid = newLogin.guid;
+ loginItem.encType = encType;
+ loginItem.timeCreated = newLogin.timeCreated;
+ loginItem.timeLastUsed = newLogin.timeLastUsed;
+ loginItem.timePasswordChanged = newLogin.timePasswordChanged;
+ loginItem.timesUsed = newLogin.timesUsed;
+ this._store.saveSoon();
+ break;
+ }
+ }
+
+ LoginHelper.notifyStorageChanged("modifyLogin", [oldStoredLogin, newLogin]);
+ },
+
+ /**
+ * @return {nsILoginInfo[]}
+ */
+ getAllLogins(count) {
+ let [logins, ids] = this._searchLogins({});
+
+ // decrypt entries for caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_getAllLogins: returning", logins.length, "logins.");
+ if (count)
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins(count, matchData) {
+ let realMatchData = {};
+ let options = {};
+ // Convert nsIPropertyBag to normal JS object
+ let propEnum = matchData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // Some property names aren't field names but are special options to affect the search.
+ case "schemeUpgrades": {
+ options[prop.name] = prop.value;
+ break;
+ }
+ default: {
+ realMatchData[prop.name] = prop.value;
+ break;
+ }
+ }
+ }
+
+ let [logins, ids] = this._searchLogins(realMatchData, options);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ /**
+ * Private method to perform arbitrary searches on any field. Decryption is
+ * left to the caller.
+ *
+ * Returns [logins, ids] for logins that match the arguments, where logins
+ * is an array of encrypted nsLoginInfo and ids is an array of associated
+ * ids in the database.
+ */
+ _searchLogins(matchData, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ this._store.ensureDataReady();
+
+ function match(aLogin) {
+ for (let field in matchData) {
+ let wantedValue = matchData[field];
+ switch (field) {
+ case "formSubmitURL":
+ if (wantedValue != null) {
+ // Historical compatibility requires this special case
+ if (aLogin.formSubmitURL == "") {
+ break;
+ }
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
+ return false;
+ }
+ break;
+ }
+ // fall through
+ case "hostname":
+ if (wantedValue != null) { // needed for formSubmitURL fall through
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
+ return false;
+ }
+ break;
+ }
+ // fall through
+ // Normal cases.
+ case "httpRealm":
+ case "id":
+ case "usernameField":
+ case "passwordField":
+ case "encryptedUsername":
+ case "encryptedPassword":
+ case "guid":
+ case "encType":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ if (wantedValue == null && aLogin[field]) {
+ return false;
+ } else if (aLogin[field] != wantedValue) {
+ return false;
+ }
+ break;
+ // Fail if caller requests an unknown property.
+ default:
+ throw new Error("Unexpected field: " + field);
+ }
+ }
+ return true;
+ }
+
+ let foundLogins = [], foundIds = [];
+ for (let loginItem of this._store.data.logins) {
+ if (match(loginItem)) {
+ // Create the new nsLoginInfo object, push to array
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login.init(loginItem.hostname, loginItem.formSubmitURL,
+ loginItem.httpRealm, loginItem.encryptedUsername,
+ loginItem.encryptedPassword, loginItem.usernameField,
+ loginItem.passwordField);
+ // set nsILoginMetaInfo values
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.guid = loginItem.guid;
+ login.timeCreated = loginItem.timeCreated;
+ login.timeLastUsed = loginItem.timeLastUsed;
+ login.timePasswordChanged = loginItem.timePasswordChanged;
+ login.timesUsed = loginItem.timesUsed;
+ foundLogins.push(login);
+ foundIds.push(loginItem.id);
+ }
+ }
+
+ this.log("_searchLogins: returning", foundLogins.length, "logins for", matchData,
+ "with options", aOptions);
+ return [foundLogins, foundIds];
+ },
+
+ /**
+ * Removes all logins from storage.
+ */
+ removeAllLogins() {
+ this._store.ensureDataReady();
+
+ this.log("Removing all logins");
+ this._store.data.logins = [];
+ this._store.saveSoon();
+
+ LoginHelper.notifyStorageChanged("removeAllLogins", null);
+ },
+
+ findLogins(count, hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_findLogins: returning", logins.length, "logins");
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ countLogins(hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ this.log("_countLogins: counted logins:", logins.length);
+ return logins.length;
+ },
+
+ get uiBusy() {
+ return this._crypto.uiBusy;
+ },
+
+ get isLoggedIn() {
+ return this._crypto.isLoggedIn;
+ },
+
+ /**
+ * Returns an array with two items: [id, login]. If the login was not
+ * found, both items will be null. The returned login contains the actual
+ * stored login (useful for looking at the actual nsILoginMetaInfo values).
+ */
+ _getIdForLogin(login) {
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (login[field] != '')
+ matchData[field] = login[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ let id = null;
+ let foundLogin = null;
+
+ // The specified login isn't encrypted, so we need to ensure
+ // the logins we're comparing with are decrypted. We decrypt one entry
+ // at a time, lest _decryptLogins return fewer entries and screw up
+ // indices between the two.
+ for (let i = 0; i < logins.length; i++) {
+ let [decryptedLogin] = this._decryptLogins([logins[i]]);
+
+ if (!decryptedLogin || !decryptedLogin.equals(login))
+ continue;
+
+ // We've found a match, set id and break
+ foundLogin = decryptedLogin;
+ id = ids[i];
+ break;
+ }
+
+ return [id, foundLogin];
+ },
+
+ /**
+ * Checks to see if the specified GUID already exists.
+ */
+ _isGuidUnique(guid) {
+ this._store.ensureDataReady();
+
+ return this._store.data.logins.every(l => l.guid != guid);
+ },
+
+ /**
+ * Returns the encrypted username, password, and encrypton type for the specified
+ * login. Can throw if the user cancels a master password entry.
+ */
+ _encryptLogin(login) {
+ let encUsername = this._crypto.encrypt(login.username);
+ let encPassword = this._crypto.encrypt(login.password);
+ let encType = this._crypto.defaultEncType;
+
+ return [encUsername, encPassword, encType];
+ },
+
+ /**
+ * Decrypts username and password fields in the provided array of
+ * logins.
+ *
+ * The entries specified by the array will be decrypted, if possible.
+ * An array of successfully decrypted logins will be returned. The return
+ * value should be given to external callers (since still-encrypted
+ * entries are useless), whereas internal callers generally don't want
+ * to lose unencrypted entries (eg, because the user clicked Cancel
+ * instead of entering their master password)
+ */
+ _decryptLogins(logins) {
+ let result = [];
+
+ for (let login of logins) {
+ try {
+ login.username = this._crypto.decrypt(login.username);
+ login.password = this._crypto.decrypt(login.password);
+ } catch (e) {
+ // If decryption failed (corrupt entry?), just skip it.
+ // Rethrow other errors (like canceling entry of a master pw)
+ if (e.result == Cr.NS_ERROR_FAILURE)
+ continue;
+ throw e;
+ }
+ result.push(login);
+ }
+
+ return result;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_json.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login storage");
+ return logger.log.bind(logger);
+});
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManagerStorage_json]);
diff --git a/toolkit/components/passwordmgr/storage-mozStorage.js b/toolkit/components/passwordmgr/storage-mozStorage.js
new file mode 100644
index 000000000..7fc9e57fd
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-mozStorage.js
@@ -0,0 +1,1262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const DB_VERSION = 6; // The database schema version
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+/**
+ * Object that manages a database transaction properly so consumers don't have
+ * to worry about it throwing.
+ *
+ * @param aDatabase
+ * The mozIStorageConnection to start a transaction on.
+ */
+function Transaction(aDatabase) {
+ this._db = aDatabase;
+
+ this._hasTransaction = false;
+ try {
+ this._db.beginTransaction();
+ this._hasTransaction = true;
+ } catch (e) { /* om nom nom exceptions */ }
+}
+
+Transaction.prototype = {
+ commit : function() {
+ if (this._hasTransaction)
+ this._db.commitTransaction();
+ },
+
+ rollback : function() {
+ if (this._hasTransaction)
+ this._db.rollbackTransaction();
+ },
+};
+
+
+function LoginManagerStorage_mozStorage() { }
+
+LoginManagerStorage_mozStorage.prototype = {
+
+ classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
+ Ci.nsIInterfaceRequestor]),
+ getInterface : function(aIID) {
+ if (aIID.equals(Ci.nsIVariant)) {
+ // Allows unwrapping the JavaScript object for regression tests.
+ return this;
+ }
+
+ if (aIID.equals(Ci.mozIStorageConnection)) {
+ return this._dbConnection;
+ }
+
+ throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ __crypto : null, // nsILoginManagerCrypto service
+ get _crypto() {
+ if (!this.__crypto)
+ this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
+ getService(Ci.nsILoginManagerCrypto);
+ return this.__crypto;
+ },
+
+ __profileDir: null, // nsIFile for the user's profile dir
+ get _profileDir() {
+ if (!this.__profileDir)
+ this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ return this.__profileDir;
+ },
+
+ __storageService: null, // Storage service for using mozStorage
+ get _storageService() {
+ if (!this.__storageService)
+ this.__storageService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ return this.__storageService;
+ },
+
+ __uuidService: null,
+ get _uuidService() {
+ if (!this.__uuidService)
+ this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ return this.__uuidService;
+ },
+
+
+ // The current database schema.
+ _dbSchema: {
+ tables: {
+ moz_logins: "id INTEGER PRIMARY KEY," +
+ "hostname TEXT NOT NULL," +
+ "httpRealm TEXT," +
+ "formSubmitURL TEXT," +
+ "usernameField TEXT NOT NULL," +
+ "passwordField TEXT NOT NULL," +
+ "encryptedUsername TEXT NOT NULL," +
+ "encryptedPassword TEXT NOT NULL," +
+ "guid TEXT," +
+ "encType INTEGER," +
+ "timeCreated INTEGER," +
+ "timeLastUsed INTEGER," +
+ "timePasswordChanged INTEGER," +
+ "timesUsed INTEGER",
+ // Changes must be reflected in this._dbAreExpectedColumnsPresent(),
+ // this._searchLogins(), and this.modifyLogin().
+
+ moz_disabledHosts: "id INTEGER PRIMARY KEY," +
+ "hostname TEXT UNIQUE ON CONFLICT REPLACE",
+
+ moz_deleted_logins: "id INTEGER PRIMARY KEY," +
+ "guid TEXT," +
+ "timeDeleted INTEGER",
+ },
+ indices: {
+ moz_logins_hostname_index: {
+ table: "moz_logins",
+ columns: ["hostname"]
+ },
+ moz_logins_hostname_formSubmitURL_index: {
+ table: "moz_logins",
+ columns: ["hostname", "formSubmitURL"]
+ },
+ moz_logins_hostname_httpRealm_index: {
+ table: "moz_logins",
+ columns: ["hostname", "httpRealm"]
+ },
+ moz_logins_guid_index: {
+ table: "moz_logins",
+ columns: ["guid"]
+ },
+ moz_logins_encType_index: {
+ table: "moz_logins",
+ columns: ["encType"]
+ }
+ }
+ },
+ _dbConnection : null, // The database connection
+ _dbStmts : null, // Database statements for memoization
+
+ _signonsFile : null, // nsIFile for "signons.sqlite"
+
+
+ /*
+ * Internal method used by regression tests only. It overrides the default
+ * database location.
+ */
+ initWithFile : function(aDBFile) {
+ if (aDBFile)
+ this._signonsFile = aDBFile;
+
+ this.initialize();
+ },
+
+
+ initialize : function () {
+ this._dbStmts = {};
+
+ let isFirstRun;
+ try {
+ // Force initialization of the crypto module.
+ // See bug 717490 comment 17.
+ this._crypto;
+
+ // If initWithFile is calling us, _signonsFile may already be set.
+ if (!this._signonsFile) {
+ // Initialize signons.sqlite
+ this._signonsFile = this._profileDir.clone();
+ this._signonsFile.append("signons.sqlite");
+ }
+ this.log("Opening database at " + this._signonsFile.path);
+
+ // Initialize the database (create, migrate as necessary)
+ isFirstRun = this._dbInit();
+
+ this._initialized = true;
+
+ return Promise.resolve();
+ } catch (e) {
+ this.log("Initialization failed: " + e);
+ // If the import fails on first run, we want to delete the db
+ if (isFirstRun && e == "Import failed")
+ this._dbCleanup(false);
+ throw new Error("Initialization failed");
+ }
+ },
+
+
+ /**
+ * Internal method used by regression tests only. It is called before
+ * replacing this storage module with a new instance.
+ */
+ terminate : function () {
+ return Promise.resolve();
+ },
+
+
+ addLogin : function (login) {
+ // Throws if there are bogus values.
+ LoginHelper.checkLoginValues(login);
+
+ let [encUsername, encPassword, encType] = this._encryptLogin(login);
+
+ // Clone the login, so we don't modify the caller's object.
+ let loginClone = login.clone();
+
+ // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
+ loginClone.QueryInterface(Ci.nsILoginMetaInfo);
+ if (loginClone.guid) {
+ if (!this._isGuidUnique(loginClone.guid))
+ throw new Error("specified GUID already exists");
+ } else {
+ loginClone.guid = this._uuidService.generateUUID().toString();
+ }
+
+ // Set timestamps
+ let currentTime = Date.now();
+ if (!loginClone.timeCreated)
+ loginClone.timeCreated = currentTime;
+ if (!loginClone.timeLastUsed)
+ loginClone.timeLastUsed = currentTime;
+ if (!loginClone.timePasswordChanged)
+ loginClone.timePasswordChanged = currentTime;
+ if (!loginClone.timesUsed)
+ loginClone.timesUsed = 1;
+
+ let query =
+ "INSERT INTO moz_logins " +
+ "(hostname, httpRealm, formSubmitURL, usernameField, " +
+ "passwordField, encryptedUsername, encryptedPassword, " +
+ "guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " +
+ "timesUsed) " +
+ "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " +
+ ":passwordField, :encryptedUsername, :encryptedPassword, " +
+ ":guid, :encType, :timeCreated, :timeLastUsed, " +
+ ":timePasswordChanged, :timesUsed)";
+
+ let params = {
+ hostname: loginClone.hostname,
+ httpRealm: loginClone.httpRealm,
+ formSubmitURL: loginClone.formSubmitURL,
+ usernameField: loginClone.usernameField,
+ passwordField: loginClone.passwordField,
+ encryptedUsername: encUsername,
+ encryptedPassword: encPassword,
+ guid: loginClone.guid,
+ encType: encType,
+ timeCreated: loginClone.timeCreated,
+ timeLastUsed: loginClone.timeLastUsed,
+ timePasswordChanged: loginClone.timePasswordChanged,
+ timesUsed: loginClone.timesUsed
+ };
+
+ let stmt;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("addLogin failed: " + e.name + " : " + e.message);
+ throw new Error("Couldn't write to database, login not added.");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Send a notification that a login was added.
+ LoginHelper.notifyStorageChanged("addLogin", loginClone);
+ return loginClone;
+ },
+
+
+ removeLogin : function (login) {
+ let [idToDelete, storedLogin] = this._getIdForLogin(login);
+ if (!idToDelete)
+ throw new Error("No matching logins");
+
+ // Execute the statement & remove from DB
+ let query = "DELETE FROM moz_logins WHERE id = :id";
+ let params = { id: idToDelete };
+ let stmt;
+ let transaction = new Transaction(this._dbConnection);
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ this.storeDeletedLogin(storedLogin);
+ transaction.commit();
+ } catch (e) {
+ this.log("_removeLogin failed: " + e.name + " : " + e.message);
+ transaction.rollback();
+ throw new Error("Couldn't write to database, login not removed.");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
+ },
+
+ modifyLogin : function (oldLogin, newLoginData) {
+ let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
+ if (!idToModify)
+ throw new Error("No matching logins");
+
+ let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
+
+ // Check if the new GUID is duplicate.
+ if (newLogin.guid != oldStoredLogin.guid &&
+ !this._isGuidUnique(newLogin.guid)) {
+ throw new Error("specified GUID already exists");
+ }
+
+ // Look for an existing entry in case key properties changed.
+ if (!newLogin.matches(oldLogin, true)) {
+ let logins = this.findLogins({}, newLogin.hostname,
+ newLogin.formSubmitURL,
+ newLogin.httpRealm);
+
+ if (logins.some(login => newLogin.matches(login, true)))
+ throw new Error("This login already exists.");
+ }
+
+ // Get the encrypted value of the username and password.
+ let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
+
+ let query =
+ "UPDATE moz_logins " +
+ "SET hostname = :hostname, " +
+ "httpRealm = :httpRealm, " +
+ "formSubmitURL = :formSubmitURL, " +
+ "usernameField = :usernameField, " +
+ "passwordField = :passwordField, " +
+ "encryptedUsername = :encryptedUsername, " +
+ "encryptedPassword = :encryptedPassword, " +
+ "guid = :guid, " +
+ "encType = :encType, " +
+ "timeCreated = :timeCreated, " +
+ "timeLastUsed = :timeLastUsed, " +
+ "timePasswordChanged = :timePasswordChanged, " +
+ "timesUsed = :timesUsed " +
+ "WHERE id = :id";
+
+ let params = {
+ id: idToModify,
+ hostname: newLogin.hostname,
+ httpRealm: newLogin.httpRealm,
+ formSubmitURL: newLogin.formSubmitURL,
+ usernameField: newLogin.usernameField,
+ passwordField: newLogin.passwordField,
+ encryptedUsername: encUsername,
+ encryptedPassword: encPassword,
+ guid: newLogin.guid,
+ encType: encType,
+ timeCreated: newLogin.timeCreated,
+ timeLastUsed: newLogin.timeLastUsed,
+ timePasswordChanged: newLogin.timePasswordChanged,
+ timesUsed: newLogin.timesUsed
+ };
+
+ let stmt;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("modifyLogin failed: " + e.name + " : " + e.message);
+ throw new Error("Couldn't write to database, login not modified.");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ LoginHelper.notifyStorageChanged("modifyLogin", [oldStoredLogin, newLogin]);
+ },
+
+
+ /**
+ * Returns an array of nsILoginInfo.
+ */
+ getAllLogins : function (count) {
+ let [logins, ids] = this._searchLogins({});
+
+ // decrypt entries for caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_getAllLogins: returning " + logins.length + " logins.");
+ if (count)
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins : function(count, matchData) {
+ let realMatchData = {};
+ let options = {};
+ // Convert nsIPropertyBag to normal JS object
+ let propEnum = matchData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // Some property names aren't field names but are special options to affect the search.
+ case "schemeUpgrades": {
+ options[prop.name] = prop.value;
+ break;
+ }
+ default: {
+ realMatchData[prop.name] = prop.value;
+ break;
+ }
+ }
+ }
+
+ let [logins, ids] = this._searchLogins(realMatchData, options);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ /**
+ * Private method to perform arbitrary searches on any field. Decryption is
+ * left to the caller.
+ *
+ * Returns [logins, ids] for logins that match the arguments, where logins
+ * is an array of encrypted nsLoginInfo and ids is an array of associated
+ * ids in the database.
+ */
+ _searchLogins : function (matchData, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ let conditions = [], params = {};
+
+ for (let field in matchData) {
+ let value = matchData[field];
+ let condition = "";
+ switch (field) {
+ case "formSubmitURL":
+ if (value != null) {
+ // Historical compatibility requires this special case
+ condition = "formSubmitURL = '' OR ";
+ }
+ // Fall through
+ case "hostname":
+ if (value != null) {
+ condition += `${field} = :${field}`;
+ params[field] = value;
+ let valueURI;
+ try {
+ if (aOptions.schemeUpgrades && (valueURI = Services.io.newURI(value, null, null)) &&
+ valueURI.scheme == "https") {
+ condition += ` OR ${field} = :http${field}`;
+ params["http" + field] = "http://" + valueURI.hostPort;
+ }
+ } catch (ex) {
+ // newURI will throw for some values (e.g. chrome://FirefoxAccounts)
+ // but those URLs wouldn't support upgrades anyways.
+ }
+ break;
+ }
+ // Fall through
+ // Normal cases.
+ case "httpRealm":
+ case "id":
+ case "usernameField":
+ case "passwordField":
+ case "encryptedUsername":
+ case "encryptedPassword":
+ case "guid":
+ case "encType":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ if (value == null) {
+ condition = field + " isnull";
+ } else {
+ condition = field + " = :" + field;
+ params[field] = value;
+ }
+ break;
+ // Fail if caller requests an unknown property.
+ default:
+ throw new Error("Unexpected field: " + field);
+ }
+ if (condition) {
+ conditions.push(condition);
+ }
+ }
+
+ // Build query
+ let query = "SELECT * FROM moz_logins";
+ if (conditions.length) {
+ conditions = conditions.map(c => "(" + c + ")");
+ query += " WHERE " + conditions.join(" AND ");
+ }
+
+ let stmt;
+ let logins = [], ids = [];
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ // We can't execute as usual here, since we're iterating over rows
+ while (stmt.executeStep()) {
+ // Create the new nsLoginInfo object, push to array
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login.init(stmt.row.hostname, stmt.row.formSubmitURL,
+ stmt.row.httpRealm, stmt.row.encryptedUsername,
+ stmt.row.encryptedPassword, stmt.row.usernameField,
+ stmt.row.passwordField);
+ // set nsILoginMetaInfo values
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.guid = stmt.row.guid;
+ login.timeCreated = stmt.row.timeCreated;
+ login.timeLastUsed = stmt.row.timeLastUsed;
+ login.timePasswordChanged = stmt.row.timePasswordChanged;
+ login.timesUsed = stmt.row.timesUsed;
+ logins.push(login);
+ ids.push(stmt.row.id);
+ }
+ } catch (e) {
+ this.log("_searchLogins failed: " + e.name + " : " + e.message);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ this.log("_searchLogins: returning " + logins.length + " logins");
+ return [logins, ids];
+ },
+
+ /**
+ * Moves a login to the deleted logins table
+ */
+ storeDeletedLogin : function(aLogin) {
+ let stmt = null;
+ try {
+ this.log("Storing " + aLogin.guid + " in deleted passwords\n");
+ let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)";
+ let params = { guid: aLogin.guid,
+ timeDeleted: Date.now() };
+ let stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (ex) {
+ throw ex;
+ } finally {
+ if (stmt)
+ stmt.reset();
+ }
+ },
+
+
+ /**
+ * Removes all logins from storage.
+ */
+ removeAllLogins : function () {
+ this.log("Removing all logins");
+ let query;
+ let stmt;
+ let transaction = new Transaction(this._dbConnection);
+
+ // Disabled hosts kept, as one presumably doesn't want to erase those.
+ // TODO: Add these items to the deleted items table once we've sorted
+ // out the issues from bug 756701
+ query = "DELETE FROM moz_logins";
+ try {
+ stmt = this._dbCreateStatement(query);
+ stmt.execute();
+ transaction.commit();
+ } catch (e) {
+ this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
+ transaction.rollback();
+ throw new Error("Couldn't write to database");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ LoginHelper.notifyStorageChanged("removeAllLogins", null);
+ },
+
+
+ findLogins : function (count, hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_findLogins: returning " + logins.length + " logins");
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ countLogins : function (hostname, formSubmitURL, httpRealm) {
+
+ let _countLoginsHelper = (hostname, formSubmitURL, httpRealm) => {
+ // Do checks for null and empty strings, adjust conditions and params
+ let [conditions, params] =
+ this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm);
+
+ let query = "SELECT COUNT(1) AS numLogins FROM moz_logins";
+ if (conditions.length) {
+ conditions = conditions.map(c => "(" + c + ")");
+ query += " WHERE " + conditions.join(" AND ");
+ }
+
+ let stmt, numLogins;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.executeStep();
+ numLogins = stmt.row.numLogins;
+ } catch (e) {
+ this.log("_countLogins failed: " + e.name + " : " + e.message);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ return numLogins;
+ };
+
+ let resultLogins = _countLoginsHelper(hostname, formSubmitURL, httpRealm);
+ this.log("_countLogins: counted logins: " + resultLogins);
+ return resultLogins;
+ },
+
+
+ get uiBusy() {
+ return this._crypto.uiBusy;
+ },
+
+
+ get isLoggedIn() {
+ return this._crypto.isLoggedIn;
+ },
+
+
+ /**
+ * Returns an array with two items: [id, login]. If the login was not
+ * found, both items will be null. The returned login contains the actual
+ * stored login (useful for looking at the actual nsILoginMetaInfo values).
+ */
+ _getIdForLogin : function (login) {
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (login[field] != '')
+ matchData[field] = login[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ let id = null;
+ let foundLogin = null;
+
+ // The specified login isn't encrypted, so we need to ensure
+ // the logins we're comparing with are decrypted. We decrypt one entry
+ // at a time, lest _decryptLogins return fewer entries and screw up
+ // indices between the two.
+ for (let i = 0; i < logins.length; i++) {
+ let [decryptedLogin] = this._decryptLogins([logins[i]]);
+
+ if (!decryptedLogin || !decryptedLogin.equals(login))
+ continue;
+
+ // We've found a match, set id and break
+ foundLogin = decryptedLogin;
+ id = ids[i];
+ break;
+ }
+
+ return [id, foundLogin];
+ },
+
+
+ /**
+ * Adjusts the WHERE conditions and parameters for statements prior to the
+ * statement being created. This fixes the cases where nulls are involved
+ * and the empty string is supposed to be a wildcard match
+ */
+ _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) {
+ let conditions = [], params = {};
+
+ if (hostname == null) {
+ conditions.push("hostname isnull");
+ } else if (hostname != '') {
+ conditions.push("hostname = :hostname");
+ params["hostname"] = hostname;
+ }
+
+ if (formSubmitURL == null) {
+ conditions.push("formSubmitURL isnull");
+ } else if (formSubmitURL != '') {
+ conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
+ params["formSubmitURL"] = formSubmitURL;
+ }
+
+ if (httpRealm == null) {
+ conditions.push("httpRealm isnull");
+ } else if (httpRealm != '') {
+ conditions.push("httpRealm = :httpRealm");
+ params["httpRealm"] = httpRealm;
+ }
+
+ return [conditions, params];
+ },
+
+
+ /**
+ * Checks to see if the specified GUID already exists.
+ */
+ _isGuidUnique : function (guid) {
+ let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid";
+ let params = { guid: guid };
+
+ let stmt, numLogins;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.executeStep();
+ numLogins = stmt.row.numLogins;
+ } catch (e) {
+ this.log("_isGuidUnique failed: " + e.name + " : " + e.message);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ return (numLogins == 0);
+ },
+
+
+ /**
+ * Returns the encrypted username, password, and encrypton type for the specified
+ * login. Can throw if the user cancels a master password entry.
+ */
+ _encryptLogin : function (login) {
+ let encUsername = this._crypto.encrypt(login.username);
+ let encPassword = this._crypto.encrypt(login.password);
+ let encType = this._crypto.defaultEncType;
+
+ return [encUsername, encPassword, encType];
+ },
+
+
+ /**
+ * Decrypts username and password fields in the provided array of
+ * logins.
+ *
+ * The entries specified by the array will be decrypted, if possible.
+ * An array of successfully decrypted logins will be returned. The return
+ * value should be given to external callers (since still-encrypted
+ * entries are useless), whereas internal callers generally don't want
+ * to lose unencrypted entries (eg, because the user clicked Cancel
+ * instead of entering their master password)
+ */
+ _decryptLogins : function (logins) {
+ let result = [];
+
+ for (let login of logins) {
+ try {
+ login.username = this._crypto.decrypt(login.username);
+ login.password = this._crypto.decrypt(login.password);
+ } catch (e) {
+ // If decryption failed (corrupt entry?), just skip it.
+ // Rethrow other errors (like canceling entry of a master pw)
+ if (e.result == Cr.NS_ERROR_FAILURE)
+ continue;
+ throw e;
+ }
+ result.push(login);
+ }
+
+ return result;
+ },
+
+
+ // Database Creation & Access
+
+ /**
+ * Creates a statement, wraps it, and then does parameter replacement
+ * Returns the wrapped statement for execution. Will use memoization
+ * so that statements can be reused.
+ */
+ _dbCreateStatement : function (query, params) {
+ let wrappedStmt = this._dbStmts[query];
+ // Memoize the statements
+ if (!wrappedStmt) {
+ this.log("Creating new statement for query: " + query);
+ wrappedStmt = this._dbConnection.createStatement(query);
+ this._dbStmts[query] = wrappedStmt;
+ }
+ // Replace parameters, must be done 1 at a time
+ if (params)
+ for (let i in params)
+ wrappedStmt.params[i] = params[i];
+ return wrappedStmt;
+ },
+
+
+ /**
+ * Attempts to initialize the database. This creates the file if it doesn't
+ * exist, performs any migrations, etc. Return if this is the first run.
+ */
+ _dbInit : function () {
+ this.log("Initializing Database");
+ let isFirstRun = false;
+ try {
+ this._dbConnection = this._storageService.openDatabase(this._signonsFile);
+ // Get the version of the schema in the file. It will be 0 if the
+ // database has not been created yet.
+ let version = this._dbConnection.schemaVersion;
+ if (version == 0) {
+ this._dbCreate();
+ isFirstRun = true;
+ } else if (version != DB_VERSION) {
+ this._dbMigrate(version);
+ }
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
+ // Database is corrupted, so we backup the database, then throw
+ // causing initialization to fail and a new db to be created next use
+ this._dbCleanup(true);
+ }
+ throw e;
+ }
+
+ Services.obs.addObserver(this, "profile-before-change", false);
+ return isFirstRun;
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "profile-before-change":
+ Services.obs.removeObserver(this, "profile-before-change");
+ this._dbClose();
+ break;
+ }
+ },
+
+ _dbCreate: function () {
+ this.log("Creating Database");
+ this._dbCreateSchema();
+ this._dbConnection.schemaVersion = DB_VERSION;
+ },
+
+
+ _dbCreateSchema : function () {
+ this._dbCreateTables();
+ this._dbCreateIndices();
+ },
+
+
+ _dbCreateTables : function () {
+ this.log("Creating Tables");
+ for (let name in this._dbSchema.tables)
+ this._dbConnection.createTable(name, this._dbSchema.tables[name]);
+ },
+
+
+ _dbCreateIndices : function () {
+ this.log("Creating Indices");
+ for (let name in this._dbSchema.indices) {
+ let index = this._dbSchema.indices[name];
+ let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
+ "(" + index.columns.join(", ") + ")";
+ this._dbConnection.executeSimpleSQL(statement);
+ }
+ },
+
+
+ _dbMigrate : function (oldVersion) {
+ this.log("Attempting to migrate from version " + oldVersion);
+
+ if (oldVersion > DB_VERSION) {
+ this.log("Downgrading to version " + DB_VERSION);
+ // User's DB is newer. Sanity check that our expected columns are
+ // present, and if so mark the lower version and merrily continue
+ // on. If the columns are borked, something is wrong so blow away
+ // the DB and start from scratch. [Future incompatible upgrades
+ // should swtich to a different table or file.]
+
+ if (!this._dbAreExpectedColumnsPresent())
+ throw Components.Exception("DB is missing expected columns",
+ Cr.NS_ERROR_FILE_CORRUPTED);
+
+ // Change the stored version to the current version. If the user
+ // runs the newer code again, it will see the lower version number
+ // and re-upgrade (to fixup any entries the old code added).
+ this._dbConnection.schemaVersion = DB_VERSION;
+ return;
+ }
+
+ // Upgrade to newer version...
+
+ let transaction = new Transaction(this._dbConnection);
+
+ try {
+ for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
+ this.log("Upgrading to version " + v + "...");
+ let migrateFunction = "_dbMigrateToVersion" + v;
+ this[migrateFunction]();
+ }
+ } catch (e) {
+ this.log("Migration failed: " + e);
+ transaction.rollback();
+ throw e;
+ }
+
+ this._dbConnection.schemaVersion = DB_VERSION;
+ transaction.commit();
+ this.log("DB migration completed.");
+ },
+
+
+ /**
+ * Version 2 adds a GUID column. Existing logins are assigned a random GUID.
+ */
+ _dbMigrateToVersion2 : function () {
+ // Check to see if GUID column already exists, add if needed
+ let query;
+ if (!this._dbColumnExists("guid")) {
+ query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT";
+ this._dbConnection.executeSimpleSQL(query);
+
+ query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)";
+ this._dbConnection.executeSimpleSQL(query);
+ }
+
+ // Get a list of IDs for existing logins
+ let ids = [];
+ query = "SELECT id FROM moz_logins WHERE guid isnull";
+ let stmt;
+ try {
+ stmt = this._dbCreateStatement(query);
+ while (stmt.executeStep())
+ ids.push(stmt.row.id);
+ } catch (e) {
+ this.log("Failed getting IDs: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Generate a GUID for each login and update the DB.
+ query = "UPDATE moz_logins SET guid = :guid WHERE id = :id";
+ for (let id of ids) {
+ let params = {
+ id: id,
+ guid: this._uuidService.generateUUID().toString()
+ };
+
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting GUID: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Version 3 adds a encType column.
+ */
+ _dbMigrateToVersion3 : function () {
+ // Check to see if encType column already exists, add if needed
+ let query;
+ if (!this._dbColumnExists("encType")) {
+ query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER";
+ this._dbConnection.executeSimpleSQL(query);
+
+ query = "CREATE INDEX IF NOT EXISTS " +
+ "moz_logins_encType_index ON moz_logins (encType)";
+ this._dbConnection.executeSimpleSQL(query);
+ }
+
+ // Get a list of existing logins
+ let logins = [];
+ let stmt;
+ query = "SELECT id, encryptedUsername, encryptedPassword " +
+ "FROM moz_logins WHERE encType isnull";
+ try {
+ stmt = this._dbCreateStatement(query);
+ while (stmt.executeStep()) {
+ let params = { id: stmt.row.id };
+ // We will tag base64 logins correctly, but no longer support their use.
+ if (stmt.row.encryptedUsername.charAt(0) == '~' ||
+ stmt.row.encryptedPassword.charAt(0) == '~')
+ params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64;
+ else
+ params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
+ logins.push(params);
+ }
+ } catch (e) {
+ this.log("Failed getting logins: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Determine encryption type for each login and update the DB.
+ query = "UPDATE moz_logins SET encType = :encType WHERE id = :id";
+ for (let params of logins) {
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting encType: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Version 4 adds timeCreated, timeLastUsed, timePasswordChanged,
+ * and timesUsed columns
+ */
+ _dbMigrateToVersion4 : function () {
+ let query;
+ // Add the new columns, if needed.
+ for (let column of ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
+ if (!this._dbColumnExists(column)) {
+ query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER";
+ this._dbConnection.executeSimpleSQL(query);
+ }
+ }
+
+ // Get a list of IDs for existing logins.
+ let ids = [];
+ let stmt;
+ query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " +
+ "timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull";
+ try {
+ stmt = this._dbCreateStatement(query);
+ while (stmt.executeStep())
+ ids.push(stmt.row.id);
+ } catch (e) {
+ this.log("Failed getting IDs: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Initialize logins with current time.
+ query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " +
+ "timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id";
+ let params = {
+ id: null,
+ initTime: Date.now()
+ };
+ for (let id of ids) {
+ params.id = id;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting timestamps: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Version 5 adds the moz_deleted_logins table
+ */
+ _dbMigrateToVersion5 : function () {
+ if (!this._dbConnection.tableExists("moz_deleted_logins")) {
+ this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins);
+ }
+ },
+
+ /**
+ * Version 6 migrates all the hosts from
+ * moz_disabledHosts to the permission manager.
+ */
+ _dbMigrateToVersion6 : function () {
+ let disabledHosts = [];
+ let query = "SELECT hostname FROM moz_disabledHosts";
+ let stmt;
+
+ try {
+ stmt = this._dbCreateStatement(query);
+
+ while (stmt.executeStep()) {
+ disabledHosts.push(stmt.row.hostname);
+ }
+
+ for (let host of disabledHosts) {
+ try {
+ let uri = Services.io.newURI(host, null, null);
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ } catch (e) {
+ this.log(`_dbMigrateToVersion6 failed: ${e.name} : ${e.message}`);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ query = "DELETE FROM moz_disabledHosts";
+ this._dbConnection.executeSimpleSQL(query);
+ },
+
+ /**
+ * Sanity check to ensure that the columns this version of the code expects
+ * are present in the DB we're using.
+ */
+ _dbAreExpectedColumnsPresent : function () {
+ let query = "SELECT " +
+ "id, " +
+ "hostname, " +
+ "httpRealm, " +
+ "formSubmitURL, " +
+ "usernameField, " +
+ "passwordField, " +
+ "encryptedUsername, " +
+ "encryptedPassword, " +
+ "guid, " +
+ "encType, " +
+ "timeCreated, " +
+ "timeLastUsed, " +
+ "timePasswordChanged, " +
+ "timesUsed " +
+ "FROM moz_logins";
+ try {
+ let stmt = this._dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ } catch (e) {
+ return false;
+ }
+
+ query = "SELECT " +
+ "id, " +
+ "hostname " +
+ "FROM moz_disabledHosts";
+ try {
+ let stmt = this._dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ } catch (e) {
+ return false;
+ }
+
+ this.log("verified that expected columns are present in DB.");
+ return true;
+ },
+
+
+ /**
+ * Checks to see if the named column already exists.
+ */
+ _dbColumnExists : function (columnName) {
+ let query = "SELECT " + columnName + " FROM moz_logins";
+ try {
+ let stmt = this._dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ _dbClose : function () {
+ this.log("Closing the DB connection.");
+ // Finalize all statements to free memory, avoid errors later
+ for (let query in this._dbStmts) {
+ let stmt = this._dbStmts[query];
+ stmt.finalize();
+ }
+ this._dbStmts = {};
+
+ if (this._dbConnection !== null) {
+ try {
+ this._dbConnection.close();
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+ this._dbConnection = null;
+ },
+
+ /**
+ * Called when database creation fails. Finalizes database statements,
+ * closes the database connection, deletes the database file.
+ */
+ _dbCleanup : function (backup) {
+ this.log("Cleaning up DB file - close & remove & backup=" + backup);
+
+ // Create backup file
+ if (backup) {
+ let backupFile = this._signonsFile.leafName + ".corrupt";
+ this._storageService.backupDatabaseFile(this._signonsFile, backupFile);
+ }
+
+ this._dbClose();
+ this._signonsFile.remove(false);
+ }
+
+}; // end of nsLoginManagerStorage_mozStorage implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_mozStorage.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login storage");
+ return logger.log.bind(logger);
+});
+
+var component = [LoginManagerStorage_mozStorage];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/passwordmgr/test/.eslintrc.js b/toolkit/components/passwordmgr/test/.eslintrc.js
new file mode 100644
index 000000000..ca626f31c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/.eslintrc.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../testing/mochitest/mochitest.eslintrc.js",
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ],
+ "rules": {
+ "brace-style": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ },
+};
diff --git a/toolkit/components/passwordmgr/test/LoginTestUtils.jsm b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
new file mode 100644
index 000000000..2fd8a31a3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
@@ -0,0 +1,295 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Shared functions generally available for testing login components.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginTestUtils",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.import("resource://testing-common/TestUtils.jsm");
+
+const LoginInfo =
+ Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo", "init");
+
+// For now, we need consumers to provide a reference to Assert.jsm.
+var Assert = null;
+
+this.LoginTestUtils = {
+ set Assert(assert) {
+ Assert = assert; // eslint-disable-line no-native-reassign
+ },
+
+ /**
+ * Forces the storage module to save all data, and the Login Manager service
+ * to replace the storage module with a newly initialized instance.
+ */
+ * reloadData() {
+ Services.obs.notifyObservers(null, "passwordmgr-storage-replace", null);
+ yield TestUtils.topicObserved("passwordmgr-storage-replace-complete");
+ },
+
+ /**
+ * Erases all the data stored by the Login Manager service.
+ */
+ clearData() {
+ Services.logins.removeAllLogins();
+ for (let hostname of Services.logins.getAllDisabledHosts()) {
+ Services.logins.setLoginSavingEnabled(hostname, true);
+ }
+ },
+
+ /**
+ * Checks that the currently stored list of nsILoginInfo matches the provided
+ * array. The comparison uses the "equals" method of nsILoginInfo, that does
+ * not include nsILoginMetaInfo properties in the test.
+ */
+ checkLogins(expectedLogins) {
+ this.assertLoginListsEqual(Services.logins.getAllLogins(), expectedLogins);
+ },
+
+ /**
+ * Checks that the two provided arrays of nsILoginInfo have the same length,
+ * and every login in "expected" is also found in "actual". The comparison
+ * uses the "equals" method of nsILoginInfo, that does not include
+ * nsILoginMetaInfo properties in the test.
+ */
+ assertLoginListsEqual(actual, expected) {
+ Assert.equal(expected.length, actual.length);
+ Assert.ok(expected.every(e => actual.some(a => a.equals(e))));
+ },
+
+ /**
+ * Checks that the two provided arrays of strings contain the same values,
+ * maybe in a different order, case-sensitively.
+ */
+ assertDisabledHostsEqual(actual, expected) {
+ Assert.deepEqual(actual.sort(), expected.sort());
+ },
+
+ /**
+ * Checks whether the given time, expressed as the number of milliseconds
+ * since January 1, 1970, 00:00:00 UTC, falls within 30 seconds of now.
+ */
+ assertTimeIsAboutNow(timeMs) {
+ Assert.ok(Math.abs(timeMs - Date.now()) < 30000);
+ },
+};
+
+/**
+ * This object contains functions that return new instances of nsILoginInfo for
+ * every call. The returned instances can be compared using their "equals" or
+ * "matches" methods, or modified for the needs of the specific test being run.
+ *
+ * Any modification to the test data requires updating the tests accordingly, in
+ * particular the search tests.
+ */
+this.LoginTestUtils.testData = {
+ /**
+ * Returns a new nsILoginInfo for use with form submits.
+ *
+ * @param modifications
+ * Each property of this object replaces the property of the same name
+ * in the returned nsILoginInfo or nsILoginMetaInfo.
+ */
+ formLogin(modifications) {
+ let loginInfo = new LoginInfo("http://www3.example.com",
+ "http://www.example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password");
+ loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+ if (modifications) {
+ for (let [name, value] of Object.entries(modifications)) {
+ loginInfo[name] = value;
+ }
+ }
+ return loginInfo;
+ },
+
+ /**
+ * Returns a new nsILoginInfo for use with HTTP authentication.
+ *
+ * @param modifications
+ * Each property of this object replaces the property of the same name
+ * in the returned nsILoginInfo or nsILoginMetaInfo.
+ */
+ authLogin(modifications) {
+ let loginInfo = new LoginInfo("http://www.example.org", null,
+ "The HTTP Realm", "the username",
+ "the password", "", "");
+ loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+ if (modifications) {
+ for (let [name, value] of Object.entries(modifications)) {
+ loginInfo[name] = value;
+ }
+ }
+ return loginInfo;
+ },
+
+ /**
+ * Returns an array of typical nsILoginInfo that could be stored in the
+ * database.
+ */
+ loginList() {
+ return [
+ // --- Examples of form logins (subdomains of example.com) ---
+
+ // Simple form login with named fields for username and password.
+ new LoginInfo("http://www.example.com", "http://www.example.com", null,
+ "the username", "the password for www.example.com",
+ "form_field_username", "form_field_password"),
+
+ // Different schemes are treated as completely different sites.
+ new LoginInfo("https://www.example.com", "https://www.example.com", null,
+ "the username", "the password for https",
+ "form_field_username", "form_field_password"),
+
+ // Subdomains are treated as completely different sites.
+ new LoginInfo("https://example.com", "https://example.com", null,
+ "the username", "the password for example.com",
+ "form_field_username", "form_field_password"),
+
+ // Forms found on the same host, but with different hostnames in the
+ // "action" attribute, are handled independently.
+ new LoginInfo("http://www3.example.com", "http://www.example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www3.example.com", "https://www.example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www3.example.com", "http://example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+
+ // It is not possible to store multiple passwords for the same username,
+ // however multiple passwords can be stored when the usernames differ.
+ // An empty username is a valid case and different from the others.
+ new LoginInfo("http://www4.example.com", "http://www4.example.com", null,
+ "username one", "password one",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www4.example.com", "http://www4.example.com", null,
+ "username two", "password two",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www4.example.com", "http://www4.example.com", null,
+ "", "password three",
+ "form_field_username", "form_field_password"),
+
+ // Username and passwords fields in forms may have no "name" attribute.
+ new LoginInfo("http://www5.example.com", "http://www5.example.com", null,
+ "multi username", "multi password", "", ""),
+
+ // Forms with PIN-type authentication will typically have no username.
+ new LoginInfo("http://www6.example.com", "http://www6.example.com", null,
+ "", "12345", "", "form_field_password"),
+
+ // --- Examples of authentication logins (subdomains of example.org) ---
+
+ // Simple HTTP authentication login.
+ new LoginInfo("http://www.example.org", null, "The HTTP Realm",
+ "the username", "the password", "", ""),
+
+ // Simple FTP authentication login.
+ new LoginInfo("ftp://ftp.example.org", null, "ftp://ftp.example.org",
+ "the username", "the password", "", ""),
+
+ // Multiple HTTP authentication logins can be stored for different realms.
+ new LoginInfo("http://www2.example.org", null, "The HTTP Realm",
+ "the username", "the password", "", ""),
+ new LoginInfo("http://www2.example.org", null, "The HTTP Realm Other",
+ "the username other", "the password other", "", ""),
+
+ // --- Both form and authentication logins (example.net) ---
+
+ new LoginInfo("http://example.net", "http://example.net", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://example.net", "http://www.example.net", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://example.net", "http://www.example.net", null,
+ "username two", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://example.net", null, "The HTTP Realm",
+ "the username", "the password", "", ""),
+ new LoginInfo("http://example.net", null, "The HTTP Realm Other",
+ "username two", "the password", "", ""),
+ new LoginInfo("ftp://example.net", null, "ftp://example.net",
+ "the username", "the password", "", ""),
+
+ // --- Examples of logins added by extensions (chrome scheme) ---
+
+ new LoginInfo("chrome://example_extension", null, "Example Login One",
+ "the username", "the password one", "", ""),
+ new LoginInfo("chrome://example_extension", null, "Example Login Two",
+ "the username", "the password two", "", ""),
+ ];
+ },
+};
+
+this.LoginTestUtils.recipes = {
+ getRecipeParent() {
+ let { LoginManagerParent } = Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
+ if (!LoginManagerParent.recipeParentPromise) {
+ return null;
+ }
+ return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
+ return recipeParent;
+ });
+ },
+};
+
+this.LoginTestUtils.masterPassword = {
+ masterPassword: "omgsecret!",
+
+ _set(enable) {
+ let oldPW, newPW;
+ if (enable) {
+ oldPW = "";
+ newPW = this.masterPassword;
+ } else {
+ oldPW = this.masterPassword;
+ newPW = "";
+ }
+
+ let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"]
+ .getService(Ci.nsIPKCS11ModuleDB);
+ let slot = secmodDB.findSlotByName("");
+ if (!slot) {
+ throw new Error("Can't find slot");
+ }
+
+ // Set master password. Note that this does not log you in, so the next
+ // invocation of pwmgr can trigger a MP prompt.
+ let pk11db = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB);
+ let token = pk11db.findTokenByName("");
+ if (slot.status == Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED) {
+ dump("MP initialized to " + newPW + "\n");
+ token.initPassword(newPW);
+ } else {
+ token.checkPassword(oldPW);
+ dump("MP change from " + oldPW + " to " + newPW + "\n");
+ token.changePassword(oldPW, newPW);
+ }
+ },
+
+ enable() {
+ this._set(true);
+ },
+
+ disable() {
+ this._set(false);
+ },
+};
diff --git a/toolkit/components/passwordmgr/test/authenticate.sjs b/toolkit/components/passwordmgr/test/authenticate.sjs
new file mode 100644
index 000000000..42edc3220
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/authenticate.sjs
@@ -0,0 +1,228 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true, requestProxyAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "", expected_pass = "", realm = "mochitest";
+ var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+ var huge = false, plugin = false, anonymous = false, formauth = false;
+ var authHeaderCount = 1;
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match)
+ expected_user = match[1];
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match)
+ expected_pass = match[1];
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match)
+ realm = match[1];
+
+ // proxy_user=xxx
+ match = /proxy_user=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_user = match[1];
+
+ // proxy_pass=xxx
+ match = /proxy_pass=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_pass = match[1];
+
+ // proxy_realm=xxx
+ match = /proxy_realm=([^&]*)/.exec(query);
+ if (match)
+ proxy_realm = match[1];
+
+ // huge=1
+ match = /huge=1/.exec(query);
+ if (match)
+ huge = true;
+
+ // plugin=1
+ match = /plugin=1/.exec(query);
+ if (match)
+ plugin = true;
+
+ // multiple=1
+ match = /multiple=([^&]*)/.exec(query);
+ if (match)
+ authHeaderCount = match[1]+0;
+
+ // anonymous=1
+ match = /anonymous=1/.exec(query);
+ if (match)
+ anonymous = true;
+
+ // formauth=1
+ match = /formauth=1/.exec(query);
+ if (match)
+ formauth = true;
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ var proxy_actual_user = "", proxy_actual_pass = "";
+ if (request.hasHeader("Proxy-Authorization")) {
+ authHeader = request.getHeader("Proxy-Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ proxy_actual_user = match[1];
+ proxy_actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+ if (proxy_expected_user == proxy_actual_user &&
+ proxy_expected_pass == proxy_actual_pass) {
+ requestProxyAuth = false;
+ }
+
+ if (anonymous) {
+ if (authPresent) {
+ response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+ } else {
+ response.setStatusLine("1.0", 200, "Authorization header not found");
+ }
+ } else {
+ if (requestProxyAuth) {
+ response.setStatusLine("1.0", 407, "Proxy authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+ } else if (requestAuth) {
+ if (formauth && authPresent)
+ response.setStatusLine("1.0", 403, "Form authentication required");
+ else
+ response.setStatusLine("1.0", 401, "Authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+ }
+
+ if (plugin) {
+ response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n");
+ }
+
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/passwordmgr/test/blank.html b/toolkit/components/passwordmgr/test/blank.html
new file mode 100644
index 000000000..81ddc2235
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/blank.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/.eslintrc.js b/toolkit/components/passwordmgr/test/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/passwordmgr/test/browser/authenticate.sjs b/toolkit/components/passwordmgr/test/browser/authenticate.sjs
new file mode 100644
index 000000000..fe2d2423c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/authenticate.sjs
@@ -0,0 +1,110 @@
+function handleRequest(request, response)
+{
+ var match;
+ var requestAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "test", expected_pass = "testpass", realm = "mochitest";
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+
+ if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser.ini b/toolkit/components/passwordmgr/test/browser/browser.ini
new file mode 100644
index 000000000..b17591436
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+support-files =
+ ../formsubmit.sjs
+ authenticate.sjs
+ form_basic.html
+ form_basic_iframe.html
+ formless_basic.html
+ form_same_origin_action.html
+ form_cross_origin_secure_action.html
+ head.js
+ insecure_test.html
+ insecure_test_subframe.html
+ multiple_forms.html
+ streamConverter_content.sjs
+
+[browser_autocomplete_insecure_warning.js]
+support-files =
+ form_cross_origin_insecure_action.html
+[browser_capture_doorhanger.js]
+support-files =
+ subtst_notifications_1.html
+ subtst_notifications_2.html
+ subtst_notifications_2pw_0un.html
+ subtst_notifications_2pw_1un_1text.html
+ subtst_notifications_3.html
+ subtst_notifications_4.html
+ subtst_notifications_5.html
+ subtst_notifications_6.html
+ subtst_notifications_8.html
+ subtst_notifications_9.html
+ subtst_notifications_10.html
+ subtst_notifications_change_p.html
+[browser_capture_doorhanger_httpsUpgrade.js]
+support-files =
+ subtst_notifications_1.html
+ subtst_notifications_8.html
+[browser_capture_doorhanger_window_open.js]
+support-files =
+ subtst_notifications_11.html
+ subtst_notifications_11_popup.html
+skip-if = os == "linux" # Bug 1312981, bug 1313136
+[browser_context_menu_autocomplete_interaction.js]
+[browser_username_select_dialog.js]
+support-files =
+ subtst_notifications_change_p.html
+[browser_DOMFormHasPassword.js]
+[browser_DOMInputPasswordAdded.js]
+[browser_exceptions_dialog.js]
+[browser_formless_submit_chrome.js]
+[browser_hasInsecureLoginForms.js]
+[browser_hasInsecureLoginForms_streamConverter.js]
+[browser_http_autofill.js]
+[browser_insecurePasswordConsoleWarning.js]
+support-files =
+ form_cross_origin_insecure_action.html
+[browser_master_password_autocomplete.js]
+[browser_notifications.js]
+[browser_notifications_username.js]
+[browser_notifications_password.js]
+[browser_notifications_2.js]
+skip-if = os == "linux" # Bug 1272849 Main action button disabled state intermittent
+[browser_passwordmgr_editing.js]
+skip-if = os == "linux"
+[browser_context_menu.js]
+[browser_context_menu_iframe.js]
+[browser_passwordmgr_contextmenu.js]
+subsuite = clipboard
+[browser_passwordmgr_fields.js]
+[browser_passwordmgr_observers.js]
+[browser_passwordmgr_sort.js]
+[browser_passwordmgr_switchtab.js]
+[browser_passwordmgrdlg.js]
diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js
new file mode 100644
index 000000000..80a0dd903
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js
@@ -0,0 +1,94 @@
+const ids = {
+ INPUT_ID: "input1",
+ FORM1_ID: "form1",
+ FORM2_ID: "form2",
+ CHANGE_INPUT_ID: "input2",
+};
+
+function task(contentIds) {
+ let resolve;
+ let promise = new Promise(r => { resolve = r; });
+
+ function unexpectedContentEvent(evt) {
+ ok(false, "Received a " + evt.type + " event on content");
+ }
+
+ var gDoc = null;
+
+ addEventListener("load", tabLoad, true);
+
+ function tabLoad() {
+ if (content.location.href == "about:blank")
+ return;
+ removeEventListener("load", tabLoad, true);
+
+ gDoc = content.document;
+ gDoc.addEventListener("DOMFormHasPassword", unexpectedContentEvent, false);
+ gDoc.defaultView.setTimeout(test_inputAdd, 0);
+ }
+
+ function test_inputAdd() {
+ addEventListener("DOMFormHasPassword", test_inputAddHandler, false);
+ let input = gDoc.createElementNS("http://www.w3.org/1999/xhtml", "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentIds.INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
+ }
+
+ function test_inputAddHandler(evt) {
+ removeEventListener(evt.type, test_inputAddHandler, false);
+ is(evt.target.id, contentIds.FORM1_ID,
+ evt.type + " event targets correct form element (added password element)");
+ gDoc.defaultView.setTimeout(test_inputChangeForm, 0);
+ }
+
+ function test_inputChangeForm() {
+ addEventListener("DOMFormHasPassword", test_inputChangeFormHandler, false);
+ let input = gDoc.getElementById(contentIds.INPUT_ID);
+ input.setAttribute("form", contentIds.FORM2_ID);
+ }
+
+ function test_inputChangeFormHandler(evt) {
+ removeEventListener(evt.type, test_inputChangeFormHandler, false);
+ is(evt.target.id, contentIds.FORM2_ID,
+ evt.type + " event targets correct form element (changed form)");
+ gDoc.defaultView.setTimeout(test_inputChangesType, 0);
+ }
+
+ function test_inputChangesType() {
+ addEventListener("DOMFormHasPassword", test_inputChangesTypeHandler, false);
+ let input = gDoc.getElementById(contentIds.CHANGE_INPUT_ID);
+ input.setAttribute("type", "password");
+ }
+
+ function test_inputChangesTypeHandler(evt) {
+ removeEventListener(evt.type, test_inputChangesTypeHandler, false);
+ is(evt.target.id, contentIds.FORM1_ID,
+ evt.type + " event targets correct form element (changed type)");
+ gDoc.defaultView.setTimeout(finish, 0);
+ }
+
+ function finish() {
+ gDoc.removeEventListener("DOMFormHasPassword", unexpectedContentEvent, false);
+ resolve();
+ }
+
+ return promise;
+}
+
+add_task(function* () {
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let promise = ContentTask.spawn(tab.linkedBrowser, ids, task);
+ tab.linkedBrowser.loadURI("data:text/html;charset=utf-8," +
+ "<html><body>" +
+ "<form id='" + ids.FORM1_ID + "'>" +
+ "<input id='" + ids.CHANGE_INPUT_ID + "'></form>" +
+ "<form id='" + ids.FORM2_ID + "'></form>" +
+ "</body></html>");
+ yield promise;
+
+ ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js
new file mode 100644
index 000000000..f54892e19
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js
@@ -0,0 +1,99 @@
+const consts = {
+ HTML_NS: "http://www.w3.org/1999/xhtml",
+
+ INPUT_ID: "input1",
+ FORM1_ID: "form1",
+ FORM2_ID: "form2",
+ CHANGE_INPUT_ID: "input2",
+ BODY_INPUT_ID: "input3",
+};
+
+function task(contentConsts) {
+ let resolve;
+ let promise = new Promise(r => { resolve = r; });
+
+ function unexpectedContentEvent(evt) {
+ Assert.ok(false, "Received a " + evt.type + " event on content");
+ }
+
+ var gDoc = null;
+
+ addEventListener("load", tabLoad, true);
+
+ function tabLoad() {
+ removeEventListener("load", tabLoad, true);
+ gDoc = content.document;
+ // These events shouldn't escape to content.
+ gDoc.addEventListener("DOMInputPasswordAdded", unexpectedContentEvent, false);
+ gDoc.defaultView.setTimeout(test_inputAdd, 0);
+ }
+
+ function test_inputAdd() {
+ addEventListener("DOMInputPasswordAdded", test_inputAddHandler, false);
+ let input = gDoc.createElementNS(contentConsts.HTML_NS, "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentConsts.INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.getElementById(contentConsts.FORM1_ID).appendChild(input);
+ info("Done appending the input element");
+ }
+
+ function test_inputAddHandler(evt) {
+ removeEventListener(evt.type, test_inputAddHandler, false);
+ Assert.equal(evt.target.id, contentConsts.INPUT_ID,
+ evt.type + " event targets correct input element (added password element)");
+ gDoc.defaultView.setTimeout(test_inputAddOutsideForm, 0);
+ }
+
+ function test_inputAddOutsideForm() {
+ addEventListener("DOMInputPasswordAdded", test_inputAddOutsideFormHandler, false);
+ let input = gDoc.createElementNS(contentConsts.HTML_NS, "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentConsts.BODY_INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.body.appendChild(input);
+ info("Done appending the input element to the body");
+ }
+
+ function test_inputAddOutsideFormHandler(evt) {
+ removeEventListener(evt.type, test_inputAddOutsideFormHandler, false);
+ Assert.equal(evt.target.id, contentConsts.BODY_INPUT_ID,
+ evt.type + " event targets correct input element (added password element outside form)");
+ gDoc.defaultView.setTimeout(test_inputChangesType, 0);
+ }
+
+ function test_inputChangesType() {
+ addEventListener("DOMInputPasswordAdded", test_inputChangesTypeHandler, false);
+ let input = gDoc.getElementById(contentConsts.CHANGE_INPUT_ID);
+ input.setAttribute("type", "password");
+ }
+
+ function test_inputChangesTypeHandler(evt) {
+ removeEventListener(evt.type, test_inputChangesTypeHandler, false);
+ Assert.equal(evt.target.id, contentConsts.CHANGE_INPUT_ID,
+ evt.type + " event targets correct input element (changed type)");
+ gDoc.defaultView.setTimeout(completeTest, 0);
+ }
+
+ function completeTest() {
+ Assert.ok(true, "Test completed");
+ gDoc.removeEventListener("DOMInputPasswordAdded", unexpectedContentEvent, false);
+ resolve();
+ }
+
+ return promise;
+}
+
+add_task(function* () {
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ let promise = ContentTask.spawn(tab.linkedBrowser, consts, task);
+ tab.linkedBrowser.loadURI("data:text/html;charset=utf-8," +
+ "<html><body>" +
+ "<form id='" + consts.FORM1_ID + "'>" +
+ "<input id='" + consts.CHANGE_INPUT_ID + "'></form>" +
+ "<form id='" + consts.FORM2_ID + "'></form>" +
+ "</body></html>");
+ yield promise;
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
new file mode 100644
index 000000000..6aa8e5cf7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const EXPECTED_SUPPORT_URL = Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "insecure-password";
+
+add_task(function* test_clickInsecureFieldWarning() {
+ let url = "https://example.com" + DIRECTORY_PATH + "form_cross_origin_insecure_action.html";
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url,
+ }, function*(browser) {
+ let popup = document.getElementById("PopupAutoComplete");
+ ok(popup, "Got popup");
+
+ let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+ yield SimpleTest.promiseFocus(browser);
+ info("content window focused");
+
+ // Focus the username field to open the popup.
+ yield ContentTask.spawn(browser, null, function openAutocomplete() {
+ content.document.getElementById("form-basic-username").focus();
+ });
+
+ yield promiseShown;
+ ok(promiseShown, "autocomplete shown");
+
+ let warningItem = document.getAnonymousElementByAttribute(popup, "type", "insecureWarning");
+ ok(warningItem, "Got warning richlistitem");
+
+ yield BrowserTestUtils.waitForCondition(() => !warningItem.collapsed, "Wait for warning to show");
+
+ info("Clicking on warning");
+ let supportTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, EXPECTED_SUPPORT_URL);
+ EventUtils.synthesizeMouseAtCenter(warningItem, {});
+ let supportTab = yield supportTabPromise;
+ ok(supportTab, "Support tab opened");
+ yield BrowserTestUtils.removeTab(supportTab);
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
new file mode 100644
index 000000000..b6bfdbf50
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
@@ -0,0 +1,600 @@
+/*
+ * Test capture popup notifications
+ */
+
+const BRAND_BUNDLE = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+const BRAND_SHORT_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login2 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "", "notifyp1", "", "pass");
+let login1B = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1B", "notifyp1B", "user", "pass");
+let login2B = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "", "notifyp1B", "", "pass");
+
+requestLongerTimeout(2);
+
+add_task(function* setup() {
+ // Load recipes for this test.
+ let recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.load({
+ siteRecipes: [{
+ hosts: ["example.org"],
+ usernameSelector: "#user",
+ passwordSelector: "#pass",
+ }],
+ });
+});
+
+add_task(function* test_remember_opens() {
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+});
+
+add_task(function* test_clickNever() {
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ is(true, Services.logins.getLoginSavingEnabled("http://example.com"),
+ "Checking for login saving enabled");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, NEVER_BUTTON);
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+
+ info("Make sure Never took effect");
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ is(false, Services.logins.getLoginSavingEnabled("http://example.com"),
+ "Checking for login saving disabled");
+ Services.logins.setLoginSavingEnabled("http://example.com", true);
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_clickRemember() {
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username used on the new entry");
+ is(login.password, "notifyp1", "Check the password used on the new entry");
+ is(login.timesUsed, 1, "Check times used on new entry");
+
+ info("Make sure Remember took effect and we don't prompt for an existing login");
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username used");
+ is(login.password, "notifyp1", "Check the password used");
+ is(login.timesUsed, 2, "Check times used incremented");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+
+ // remove that login
+ Services.logins.removeLogin(login1);
+});
+
+/* signons.rememberSignons pref tests... */
+
+add_task(function* test_rememberSignonsFalse() {
+ info("Make sure we don't prompt with rememberSignons=false");
+ Services.prefs.setBoolPref("signon.rememberSignons", false);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_rememberSignonsTrue() {
+ info("Make sure we prompt with rememberSignons=true");
+ Services.prefs.setBoolPref("signon.rememberSignons", true);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+/* autocomplete=off tests... */
+
+add_task(function* test_autocompleteOffUsername() {
+ info("Check for notification popup when autocomplete=off present on username");
+
+ yield testSubmittingLoginForm("subtst_notifications_2.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_autocompleteOffPassword() {
+ info("Check for notification popup when autocomplete=off present on password");
+
+ yield testSubmittingLoginForm("subtst_notifications_3.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_autocompleteOffForm() {
+ info("Check for notification popup when autocomplete=off present on form");
+
+ yield testSubmittingLoginForm("subtst_notifications_4.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+
+add_task(function* test_noPasswordField() {
+ info("Check for no notification popup when no password field present");
+
+ yield testSubmittingLoginForm("subtst_notifications_5.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "null", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_pwOnlyLoginMatchesForm() {
+ info("Check for update popup when existing pw-only login matches form.");
+ Services.logins.addLogin(login2);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username");
+ is(login.password, "notifyp1", "Check the password");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(function* test_pwOnlyFormMatchesLogin() {
+ info("Check for no notification popup when pw-only form matches existing login.");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username");
+ is(login.password, "notifyp1", "Check the password");
+ is(login.timesUsed, 2, "Check times used");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(function* test_pwOnlyFormDoesntMatchExisting() {
+ info("Check for notification popup when pw-only form doesn't match existing login.");
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(function* test_changeUPLoginOnUPForm_dont() {
+ info("Check for change-password popup, u+p login on u+p form. (not changed)");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(function* test_changeUPLoginOnUPForm_change() {
+ info("Check for change-password popup, u+p login on u+p form.");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(function* test_changePLoginOnUPForm() {
+ info("Check for change-password popup, p-only login on u+p form.");
+ Services.logins.addLogin(login2);
+
+ yield testSubmittingLoginForm("subtst_notifications_9.html", function*(fieldValues) {
+ is(fieldValues.username, "", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ // no cleanup -- saved password to be used in the next test.
+});
+
+add_task(function* test_changePLoginOnPForm() {
+ info("Check for change-password popup, p-only login on p-only form.");
+
+ yield testSubmittingLoginForm("subtst_notifications_10.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("", "notifyp1");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password changed");
+ is(login.timesUsed, 3, "Check times used");
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(function* test_checkUPSaveText() {
+ info("Check text on a user+pass notification popup");
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ // Check the text, which comes from the localized saveLoginText string.
+ let notificationText = notif.message;
+ let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this login?";
+ is(expectedText, notificationText, "Checking text: " + notificationText);
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_checkPSaveText() {
+ info("Check text on a pass-only notification popup");
+
+ yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ // Check the text, which comes from the localized saveLoginTextNoUser string.
+ let notificationText = notif.message;
+ let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this password?";
+ is(expectedText, notificationText, "Checking text: " + notificationText);
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_capture2pw0un() {
+ info("Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there are no saved logins.");
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_change2pw0unExistingDifferentUP() {
+ info("Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with a username and different password.");
+
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(function* test_change2pw0unExistingDifferentP() {
+ info("Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with no username and different password.");
+
+ Services.logins.addLogin(login2B);
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login2B);
+});
+
+add_task(function* test_change2pw0unExistingWithSameP() {
+ info("Check for no notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with a username and the same password.");
+
+ Services.logins.addLogin(login2);
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 2, "Check times used incremented");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(function* test_changeUPLoginOnPUpdateForm() {
+ info("Check for change-password popup, u+p login on password update form.");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(function* test_recipeCaptureFields_NewLogin() {
+ info("Check that we capture the proper fields when a field recipe is in use.");
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+
+ // Sanity check, no logins should exist yet.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 0, "Should not have any logins yet");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+
+ }, "http://example.org"); // The recipe is for example.org
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+});
+
+add_task(function* test_recipeCaptureFields_ExistingLogin() {
+ info("Check that we capture the proper fields when a field recipe is in use " +
+ "and there is a matching login");
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ }, "http://example.org");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 2, "Check times used incremented");
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function* test_noShowPasswordOnDismissal() {
+ info("Check for no Show Password field when the doorhanger is dismissed");
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ info("Opening popup");
+ let notif = getCaptureDoorhanger("password-save");
+ let { panel } = PopupNotifications;
+
+ info("Hiding popup.");
+ let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ yield promiseHidden;
+
+ info("Clicking on anchor to reshow popup.");
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ notif.anchorElement.click();
+ yield promiseShown;
+
+ let passwordVisiblityToggle = panel.querySelector("#password-notification-visibilityToggle");
+ is(passwordVisiblityToggle.hidden, true, "Check that the Show Password field is Hidden");
+ });
+});
+
+// TODO:
+// * existing login test, form has different password --> change password, no save prompt
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js
new file mode 100644
index 000000000..9be0aa631
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js
@@ -0,0 +1,123 @@
+/*
+ * Test capture popup notifications with HTTPS upgrades
+ */
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login1HTTPS = new nsLoginInfo("https://example.com", "https://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+
+add_task(function* test_httpsUpgradeCaptureFields_noChange() {
+ info("Check that we don't prompt to remember when capturing an upgraded login with no change");
+ Services.logins.addLogin(login1);
+ // Sanity check the HTTP login exists.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should have the HTTP login");
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP
+
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login still");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.hostname, "http://example.com", "Check the hostname is unchanged");
+ is(login.username, "notifyu1", "Check the username is unchanged");
+ is(login.password, "notifyp1", "Check the password is unchanged");
+ is(login.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(function* test_httpsUpgradeCaptureFields_changePW() {
+ info("Check that we prompt to change when capturing an upgraded login with a new PW");
+ Services.logins.addLogin(login1);
+ // Sanity check the HTTP login exists.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should have the HTTP login");
+
+ yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "checking for a change popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login still");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.hostname, "https://example.com", "Check the hostname is upgraded");
+ is(login.formSubmitURL, "https://example.com", "Check the formSubmitURL is upgraded");
+ is(login.username, "notifyu1", "Check the username is unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function* test_httpsUpgradeCaptureFields_captureMatchingHTTP() {
+ info("Capture a new HTTP login which matches a stored HTTPS one.");
+ Services.logins.addLogin(login1HTTPS);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+
+ is(Services.logins.getAllLogins().length, 1, "Should only have the HTTPS login");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have both HTTP and HTTPS logins");
+ for (let login of logins) {
+ login = login.QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username used on the new entry");
+ is(login.password, "notifyp1", "Check the password used on the new entry");
+ is(login.timesUsed, 1, "Check times used on entry");
+ }
+
+ info("Make sure Remember took effect and we don't prompt for an existing HTTP login");
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have both HTTP and HTTPS still");
+
+ let httpsLogins = LoginHelper.searchLoginsWithObject({
+ hostname: "https://example.com",
+ });
+ is(httpsLogins.length, 1, "Check https logins count");
+ let httpsLogin = httpsLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ ok(httpsLogin.equals(login1HTTPS), "Check HTTPS login didn't change");
+ is(httpsLogin.timesUsed, 1, "Check times used");
+
+ let httpLogins = LoginHelper.searchLoginsWithObject({
+ hostname: "http://example.com",
+ });
+ is(httpLogins.length, 1, "Check http logins count");
+ let httpLogin = httpLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ ok(httpLogin.equals(login1), "Check HTTP login is as expected");
+ is(httpLogin.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeLogin(login1);
+ Services.logins.removeLogin(login1HTTPS);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js
new file mode 100644
index 000000000..1bcfec5eb
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js
@@ -0,0 +1,144 @@
+/*
+ * Test capture popup notifications in content opened by window.open
+ */
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login2 = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "notifyu2", "notifyp2", "user", "pass");
+
+
+function withTestTabUntilStorageChange(aPageFile, aTaskFn) {
+ function storageChangedObserved(subject, data) {
+ // Watch for actions triggered from a doorhanger (not cleanup tasks with removeLogin)
+ if (data == "removeLogin") {
+ return false;
+ }
+ return true;
+ }
+
+ let storageChangedPromised = TestUtils.topicObserved("passwordmgr-storage-changed",
+ storageChangedObserved);
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "http://mochi.test:8888" + DIRECTORY_PATH + aPageFile,
+ }, function*(browser) {
+ ok(true, "loaded " + aPageFile);
+ info("running test case task");
+ yield* aTaskFn();
+ info("waiting for storage change");
+ yield storageChangedPromised;
+ });
+}
+
+add_task(function* setup() {
+ yield SimpleTest.promiseFocus(window);
+});
+
+add_task(function* test_saveChromeHiddenAutoClose() {
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ // query arguments are: username, password, features, auto-close (delimited by '|')
+ let url = "subtst_notifications_11.html?notifyu1|notifyp1|" +
+ "menubar=no,toolbar=no,location=no|autoclose";
+ yield withTestTabUntilStorageChange(url, function*() {
+ info("waiting for popupshown");
+ yield notifShownPromise;
+ // the popup closes and the doorhanger should appear in the opener
+ let popup = getCaptureDoorhanger("password-save");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ // Sanity check, no logins should exist yet.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 0, "Should not have any logins yet");
+
+ clickDoorhangerButton(popup, REMEMBER_BUTTON);
+ });
+ // Check result of clicking Remember
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.timesUsed, 1, "Check times used on new entry");
+ is(login.username, "notifyu1", "Check the username used on the new entry");
+ is(login.password, "notifyp1", "Check the password used on the new entry");
+});
+
+add_task(function* test_changeChromeHiddenAutoClose() {
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ let url = "subtst_notifications_11.html?notifyu1|pass2|menubar=no,toolbar=no,location=no|autoclose";
+ yield withTestTabUntilStorageChange(url, function*() {
+ info("waiting for popupshown");
+ yield notifShownPromise;
+ let popup = getCaptureDoorhanger("password-change");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(popup, CHANGE_BUTTON);
+ });
+
+ // Check to make sure we updated the password, timestamps and use count for
+ // the login being changed with this form.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username");
+ is(login.password, "pass2", "Check password changed");
+ is(login.timesUsed, 2, "check .timesUsed incremented on change");
+ ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped");
+ ok(login.timeLastUsed == login.timePasswordChanged, "timeUsed == timeChanged");
+
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(function* test_saveChromeVisibleSameWindow() {
+ // This test actually opens a new tab in the same window with default browser settings.
+ let url = "subtst_notifications_11.html?notifyu2|notifyp2||";
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ yield withTestTabUntilStorageChange(url, function*() {
+ yield notifShownPromise;
+ let popup = getCaptureDoorhanger("password-save");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu2", "notifyp2");
+ clickDoorhangerButton(popup, REMEMBER_BUTTON);
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ // Check result of clicking Remember
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login now");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu2", "Check the username used on the new entry");
+ is(login.password, "notifyp2", "Check the password used on the new entry");
+ is(login.timesUsed, 1, "Check times used on new entry");
+});
+
+add_task(function* test_changeChromeVisibleSameWindow() {
+ let url = "subtst_notifications_11.html?notifyu2|pass2||";
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ yield withTestTabUntilStorageChange(url, function*() {
+ yield notifShownPromise;
+ let popup = getCaptureDoorhanger("password-change");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu2", "pass2");
+ clickDoorhangerButton(popup, CHANGE_BUTTON);
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ // Check to make sure we updated the password, timestamps and use count for
+ // the login being changed with this form.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu2", "Check the username");
+ is(login.password, "pass2", "Check password changed");
+ is(login.timesUsed, 2, "check .timesUsed incremented on change");
+ ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped");
+ ok(login.timeLastUsed == login.timePasswordChanged, "timeUsed == timeChanged");
+
+ // cleanup
+ login2.password = "pass2";
+ Services.logins.removeLogin(login2);
+ login2.password = "notifyp2";
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
new file mode 100644
index 000000000..6cfcaa7c2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
@@ -0,0 +1,432 @@
+/*
+ * Test the password manager context menu.
+ */
+
+/* eslint no-shadow:"off" */
+
+"use strict";
+
+// The hostname for the test URIs.
+const TEST_HOSTNAME = "https://example.com";
+const MULTIPLE_FORMS_PAGE_PATH = "/browser/toolkit/components/passwordmgr/test/browser/multiple_forms.html";
+
+const CONTEXT_MENU = document.getElementById("contentAreaContextMenu");
+const POPUP_HEADER = document.getElementById("fill-login");
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(function* test_initialize() {
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ Services.prefs.clearUserPref("signon.schemeUpgrades");
+ });
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(function* test_context_menu_populate_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(function* test_context_menu_populate_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(function* test_context_menu_populate_username_with_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-username-2");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(function* test_context_menu_populate_username_with_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-username-2");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+
+/**
+ * Check if the password field is correctly filled when one
+ * login menuitem is clicked.
+ */
+add_task(function* test_context_menu_password_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+ let formDescriptions = yield ContentTask.spawn(browser, {}, function*() {
+ let forms = Array.from(content.document.getElementsByClassName("test-form"));
+ return forms.map((f) => f.getAttribute("description"));
+ });
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+
+ let passwordInputIds = yield ContentTask.spawn(browser, {description}, function*({description}) {
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ let passwords = Array.from(formElement.querySelectorAll("input[type='password']"));
+ return passwords.map((p) => p.id);
+ });
+
+ for (let inputId of passwordInputIds) {
+ info("Testing password field: " + inputId);
+
+ // Synthesize a right mouse click over the username input element.
+ yield openPasswordContextMenu(browser, "#" + inputId, function*() {
+ let inputDisabled = yield ContentTask
+ .spawn(browser, {inputId}, function*({inputId}) {
+ let input = content.document.getElementById(inputId);
+ return input.disabled || input.readOnly;
+ });
+
+ // If the password field is disabled or read-only, we want to see
+ // the disabled Fill Password popup header.
+ if (inputDisabled) {
+ Assert.ok(!POPUP_HEADER.hidden, "Popup menu is not hidden.");
+ Assert.ok(POPUP_HEADER.disabled, "Popup menu is disabled.");
+ CONTEXT_MENU.hidePopup();
+ }
+
+ return !inputDisabled;
+ });
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ // The only field affected by the password fill
+ // should be the target password field itself.
+ yield assertContextMenuFill(browser, description, null, inputId, 1);
+ yield ContentTask.spawn(browser, {inputId}, function*({inputId}) {
+ let passwordField = content.document.getElementById(inputId);
+ Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used");
+ });
+
+ CONTEXT_MENU.hidePopup();
+ }
+ }
+ });
+});
+
+/**
+ * Check if the form is correctly filled when one
+ * username context menu login menuitem is clicked.
+ */
+add_task(function* test_context_menu_username_login_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+
+ let formDescriptions = yield ContentTask.spawn(browser, {}, function*() {
+ let forms = Array.from(content.document.getElementsByClassName("test-form"));
+ return forms.map((f) => f.getAttribute("description"));
+ });
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+ let usernameInputIds = yield ContentTask
+ .spawn(browser, {description}, function*({description}) {
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ let inputs = Array.from(formElement.querySelectorAll("input[type='text']"));
+ return inputs.map((p) => p.id);
+ });
+
+ for (let inputId of usernameInputIds) {
+ info("Testing username field: " + inputId);
+
+ // Synthesize a right mouse click over the username input element.
+ yield openPasswordContextMenu(browser, "#" + inputId, function*() {
+ let headerHidden = POPUP_HEADER.hidden;
+ let headerDisabled = POPUP_HEADER.disabled;
+
+ let data = {description, inputId, headerHidden, headerDisabled};
+ let shouldContinue = yield ContentTask.spawn(browser, data, function*(data) {
+ let {description, inputId, headerHidden, headerDisabled} = data;
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ let usernameField = content.document.getElementById(inputId);
+ // We always want to check if the first password field is filled,
+ // since this is the current behavior from the _fillForm function.
+ let passwordField = formElement.querySelector("input[type='password']");
+
+ // If we don't want to see the actual popup menu,
+ // check if the popup is hidden or disabled.
+ if (!passwordField || usernameField.disabled || usernameField.readOnly ||
+ passwordField.disabled || passwordField.readOnly) {
+ if (!passwordField) {
+ Assert.ok(headerHidden, "Popup menu is hidden.");
+ } else {
+ Assert.ok(!headerHidden, "Popup menu is not hidden.");
+ Assert.ok(headerDisabled, "Popup menu is disabled.");
+ }
+ return false;
+ }
+ return true;
+ });
+
+ if (!shouldContinue) {
+ CONTEXT_MENU.hidePopup();
+ }
+
+ return shouldContinue;
+ });
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ let passwordFieldId = yield ContentTask
+ .spawn(browser, {description}, function*({description}) {
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ return formElement.querySelector("input[type='password']").id;
+ });
+
+ // We shouldn't change any field that's not the target username field or the first password field
+ yield assertContextMenuFill(browser, description, inputId, passwordFieldId, 1);
+
+ yield ContentTask.spawn(browser, {passwordFieldId}, function*({passwordFieldId}) {
+ let passwordField = content.document.getElementById(passwordFieldId);
+ if (!passwordField.hasAttribute("expectedFail")) {
+ Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used");
+ }
+ });
+
+ CONTEXT_MENU.hidePopup();
+ }
+ }
+ });
+});
+
+/**
+ * Synthesize mouse clicks to open the password manager context menu popup
+ * for a target password input element.
+ *
+ * assertCallback should return true if we should continue or else false.
+ */
+function* openPasswordContextMenu(browser, passwordInput, assertCallback = null) {
+ // Synthesize a right mouse click over the password input element.
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(CONTEXT_MENU, "popupshown");
+ let eventDetails = {type: "contextmenu", button: 2};
+ BrowserTestUtils.synthesizeMouseAtCenter(passwordInput, eventDetails, browser);
+ yield contextMenuShownPromise;
+
+ if (assertCallback) {
+ let shouldContinue = yield assertCallback();
+ if (!shouldContinue) {
+ return;
+ }
+ }
+
+ // Synthesize a mouse click over the fill login menu header.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(POPUP_HEADER, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(POPUP_HEADER, {});
+ yield popupShownPromise;
+}
+
+/**
+ * Verify that only the expected form fields are filled.
+ */
+function* assertContextMenuFill(browser, formId, usernameFieldId, passwordFieldId, loginIndex) {
+ let popupMenu = document.getElementById("fill-login-popup");
+ let unchangedSelector = `[description="${formId}"] input:not(#${passwordFieldId})`;
+
+ if (usernameFieldId) {
+ unchangedSelector += `:not(#${usernameFieldId})`;
+ }
+
+ yield ContentTask.spawn(browser, {unchangedSelector}, function*({unchangedSelector}) {
+ let unchangedFields = content.document.querySelectorAll(unchangedSelector);
+
+ // Store the value of fields that should remain unchanged.
+ if (unchangedFields.length) {
+ for (let field of unchangedFields) {
+ field.setAttribute("original-value", field.value);
+ }
+ }
+ });
+
+ // Execute the default command of the specified login menuitem found in the context menu.
+ let loginItem = popupMenu.getElementsByClassName("context-login-item")[loginIndex];
+
+ // Find the used login by it's username (Use only unique usernames in this test).
+ let {username, password} = getLoginFromUsername(loginItem.label);
+
+ let data = {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector};
+ let continuePromise = ContentTask.spawn(browser, data, function*(data) {
+ let {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector} = data;
+ let form = content.document.querySelector(`[description="${formId}"]`);
+ yield ContentTaskUtils.waitForEvent(form, "input", "Username input value changed");
+
+ if (usernameFieldId) {
+ let usernameField = content.document.getElementById(usernameFieldId);
+
+ // If we have an username field, check if it's correctly filled
+ if (usernameField.getAttribute("expectedFail") == null) {
+ Assert.equal(username, usernameField.value, "Username filled and correct.");
+ }
+ }
+
+ if (passwordFieldId) {
+ let passwordField = content.document.getElementById(passwordFieldId);
+
+ // If we have a password field, check if it's correctly filled
+ if (passwordField && passwordField.getAttribute("expectedFail") == null) {
+ Assert.equal(password, passwordField.value, "Password filled and correct.");
+ }
+ }
+
+ let unchangedFields = content.document.querySelectorAll(unchangedSelector);
+
+ // Check that all fields that should not change have the same value as before.
+ if (unchangedFields.length) {
+ Assert.ok(() => {
+ for (let field of unchangedFields) {
+ if (field.value != field.getAttribute("original-value")) {
+ return false;
+ }
+ }
+ return true;
+ }, "Other fields were not changed.");
+ }
+ });
+
+ loginItem.doCommand();
+
+ return continuePromise;
+}
+
+/**
+ * Check if every login that matches the page hostname are available at the context menu.
+ * @param {Element} contextMenu
+ * @param {Number} expectedCount - Number of logins expected in the context menu. Used to ensure
+* we continue testing something useful.
+ */
+function checkMenu(contextMenu, expectedCount) {
+ let logins = loginList().filter(login => {
+ return LoginHelper.isOriginMatching(login.hostname, TEST_HOSTNAME, {
+ schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ });
+ });
+ // Make an array of menuitems for easier comparison.
+ let menuitems = [...CONTEXT_MENU.getElementsByClassName("context-login-item")];
+ Assert.equal(menuitems.length, expectedCount, "Expected number of menu items");
+ Assert.ok(logins.every(l => menuitems.some(m => l.username == m.label)), "Every login have an item at the menu.");
+}
+
+/**
+ * Search for a login by it's username.
+ *
+ * Only unique login/hostname combinations should be used at this test.
+ */
+function getLoginFromUsername(username) {
+ return loginList().find(login => login.username == username);
+}
+
+/**
+ * List of logins used for the test.
+ *
+ * We should only use unique usernames in this test,
+ * because we need to search logins by username. There is one duplicate u+p combo
+ * in order to test de-duping in the menu.
+ */
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ // Same as above but HTTP in order to test de-duping.
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username1",
+ password: "password1",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://example.org",
+ username: "username-cross-origin",
+ password: "password-cross-origin",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
new file mode 100644
index 000000000..1b37e3f79
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
@@ -0,0 +1,99 @@
+/*
+ * Test the password manager context menu interaction with autocomplete.
+ */
+
+"use strict";
+
+const TEST_HOSTNAME = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+
+var gUnexpectedIsTODO = false;
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(function* test_initialize() {
+ let autocompletePopup = document.getElementById("PopupAutoComplete");
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ autocompletePopup.removeEventListener("popupshowing", autocompleteUnexpectedPopupShowing);
+ });
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+ autocompletePopup.addEventListener("popupshowing", autocompleteUnexpectedPopupShowing);
+});
+
+add_task(function* test_context_menu_username() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH,
+ }, function* (browser) {
+ yield openContextMenu(browser, "#form-basic-username");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ Assert.equal(contextMenu.state, "open", "Context menu opened");
+ contextMenu.hidePopup();
+ });
+});
+
+add_task(function* test_context_menu_password() {
+ gUnexpectedIsTODO = true;
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH,
+ }, function* (browser) {
+ yield openContextMenu(browser, "#form-basic-password");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ Assert.equal(contextMenu.state, "open", "Context menu opened");
+ contextMenu.hidePopup();
+ });
+});
+
+function autocompleteUnexpectedPopupShowing(event) {
+ if (gUnexpectedIsTODO) {
+ todo(false, "Autocomplete shouldn't appear");
+ } else {
+ Assert.ok(false, "Autocomplete shouldn't appear");
+ }
+ event.target.hidePopup();
+}
+
+/**
+ * Synthesize mouse clicks to open the context menu popup
+ * for a target login input element.
+ */
+function* openContextMenu(browser, loginInput) {
+ // First synthesize a mousedown. We need this to get the focus event with the "contextmenu" event.
+ let eventDetails1 = {type: "mousedown", button: 2};
+ BrowserTestUtils.synthesizeMouseAtCenter(loginInput, eventDetails1, browser);
+
+ // Then synthesize the contextmenu click over the input element.
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown");
+ let eventDetails = {type: "contextmenu", button: 2};
+ BrowserTestUtils.synthesizeMouseAtCenter(loginInput, eventDetails, browser);
+ yield contextMenuShownPromise;
+
+ // Wait to see which popups are shown.
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+}
+
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js
new file mode 100644
index 000000000..c5219789d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js
@@ -0,0 +1,144 @@
+/*
+ * Test the password manager context menu.
+ */
+
+"use strict";
+
+const TEST_HOSTNAME = "https://example.com";
+
+// Test with a page that only has a form within an iframe, not in the top-level document
+const IFRAME_PAGE_PATH = "/browser/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html";
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(function* test_initialize() {
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ Services.prefs.clearUserPref("signon.schemeUpgrades");
+ });
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Check if the password field is correctly filled when it's in an iframe.
+ */
+add_task(function* test_context_menu_iframe_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + IFRAME_PAGE_PATH
+ }, function* (browser) {
+ function getPasswordInput() {
+ let frame = content.document.getElementById("test-iframe");
+ return frame.contentDocument.getElementById("form-basic-password");
+ }
+
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown");
+ let eventDetails = {type: "contextmenu", button: 2};
+
+ // To click at the right point we have to take into account the iframe offset.
+ // Synthesize a right mouse click over the password input element.
+ BrowserTestUtils.synthesizeMouseAtCenter(getPasswordInput, eventDetails, browser);
+ yield contextMenuShownPromise;
+
+ // Synthesize a mouse click over the fill login menu header.
+ let popupHeader = document.getElementById("fill-login");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(popupHeader, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(popupHeader, {});
+ yield popupShownPromise;
+
+ let popupMenu = document.getElementById("fill-login-popup");
+
+ // Stores the original value of username
+ function promiseFrameInputValue(name) {
+ return ContentTask.spawn(browser, name, function(inputname) {
+ let iframe = content.document.getElementById("test-iframe");
+ let input = iframe.contentDocument.getElementById(inputname);
+ return input.value;
+ });
+ }
+ let usernameOriginalValue = yield promiseFrameInputValue("form-basic-username");
+
+ // Execute the command of the first login menuitem found at the context menu.
+ let passwordChangedPromise = ContentTask.spawn(browser, null, function* () {
+ let frame = content.document.getElementById("test-iframe");
+ let passwordInput = frame.contentDocument.getElementById("form-basic-password");
+ yield ContentTaskUtils.waitForEvent(passwordInput, "input");
+ });
+
+ let firstLoginItem = popupMenu.getElementsByClassName("context-login-item")[0];
+ firstLoginItem.doCommand();
+
+ yield passwordChangedPromise;
+
+ // Find the used login by it's username.
+ let login = getLoginFromUsername(firstLoginItem.label);
+ let passwordValue = yield promiseFrameInputValue("form-basic-password");
+ is(login.password, passwordValue, "Password filled and correct.");
+
+ let usernameNewValue = yield promiseFrameInputValue("form-basic-username");
+ is(usernameOriginalValue,
+ usernameNewValue,
+ "Username value was not changed.");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ contextMenu.hidePopup();
+ });
+});
+
+/**
+ * Search for a login by it's username.
+ *
+ * Only unique login/hostname combinations should be used at this test.
+ */
+function getLoginFromUsername(username) {
+ return loginList().find(login => login.username == username);
+}
+
+/**
+ * List of logins used for the test.
+ *
+ * We should only use unique usernames in this test,
+ * because we need to search logins by username. There is one duplicate u+p combo
+ * in order to test de-duping in the menu.
+ */
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ // Same as above but HTTP in order to test de-duping.
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username1",
+ password: "password1",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://example.org",
+ username: "username-cross-origin",
+ password: "password-cross-origin",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js
new file mode 100644
index 000000000..09fbe0eea
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js
@@ -0,0 +1,56 @@
+
+"use strict";
+
+const LOGIN_HOST = "http://example.com";
+
+function openExceptionsDialog() {
+ return window.openDialog(
+ "chrome://browser/content/preferences/permissions.xul",
+ "Toolkit:PasswordManagerExceptions", "",
+ {
+ blockVisible: true,
+ sessionVisible: false,
+ allowVisible: false,
+ hideStatusColumn: true,
+ prefilledHost: "",
+ permissionType: "login-saving"
+ }
+ );
+}
+
+function countDisabledHosts(dialog) {
+ let doc = dialog.document;
+ let rejectsTree = doc.getElementById("permissionsTree");
+
+ return rejectsTree.view.rowCount;
+}
+
+function promiseStorageChanged(expectedData) {
+ function observer(subject, data) {
+ return data == expectedData && subject.QueryInterface(Ci.nsISupportsString).data == LOGIN_HOST;
+ }
+
+ return TestUtils.topicObserved("passwordmgr-storage-changed", observer);
+}
+
+add_task(function* test_disable() {
+ let dialog = openExceptionsDialog();
+ let promiseChanged = promiseStorageChanged("hostSavingDisabled");
+
+ yield BrowserTestUtils.waitForEvent(dialog, "load");
+ Services.logins.setLoginSavingEnabled(LOGIN_HOST, false);
+ yield promiseChanged;
+ is(countDisabledHosts(dialog), 1, "Verify disabled host added");
+ yield BrowserTestUtils.closeWindow(dialog);
+});
+
+add_task(function* test_enable() {
+ let dialog = openExceptionsDialog();
+ let promiseChanged = promiseStorageChanged("hostSavingEnabled");
+
+ yield BrowserTestUtils.waitForEvent(dialog, "load");
+ Services.logins.setLoginSavingEnabled(LOGIN_HOST, true);
+ yield promiseChanged;
+ is(countDisabledHosts(dialog), 0, "Verify disabled host removed");
+ yield BrowserTestUtils.closeWindow(dialog);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
new file mode 100644
index 000000000..c6d9ce50a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
@@ -0,0 +1,126 @@
+/*
+ * Test that browser chrome UI interactions don't trigger a capture doorhanger.
+ */
+
+"use strict";
+
+function* fillTestPage(aBrowser) {
+ yield ContentTask.spawn(aBrowser, null, function*() {
+ content.document.getElementById("form-basic-username").value = "my_username";
+ content.document.getElementById("form-basic-password").value = "my_password";
+ });
+ info("fields filled");
+}
+
+function* withTestPage(aTaskFn) {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com" + DIRECTORY_PATH + "formless_basic.html",
+ }, function*(aBrowser) {
+ info("tab opened");
+ yield fillTestPage(aBrowser);
+ yield* aTaskFn(aBrowser);
+
+ // Give a chance for the doorhanger to appear
+ yield new Promise(resolve => SimpleTest.executeSoon(resolve));
+ ok(!getCaptureDoorhanger("any"), "No doorhanger should be present");
+ });
+}
+
+add_task(function* setup() {
+ yield SimpleTest.promiseFocus(window);
+});
+
+add_task(function* test_urlbar_new_URL() {
+ yield withTestPage(function*(aBrowser) {
+ gURLBar.value = "";
+ let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+ gURLBar.focus();
+ yield focusPromise;
+ info("focused");
+ EventUtils.sendString("http://mochi.test:8888/");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield BrowserTestUtils.browserLoaded(aBrowser, false, "http://mochi.test:8888/");
+ });
+});
+
+add_task(function* test_urlbar_fragment_enter() {
+ yield withTestPage(function*(aBrowser) {
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ EventUtils.sendString("#fragment");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ });
+});
+
+add_task(function* test_backButton_forwardButton() {
+ yield withTestPage(function*(aBrowser) {
+ // Load a new page in the tab so we can test going back
+ aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second");
+ yield BrowserTestUtils.browserLoaded(aBrowser, false,
+ "https://example.com" + DIRECTORY_PATH +
+ "formless_basic.html?second");
+ yield fillTestPage(aBrowser);
+
+ let forwardButton = document.getElementById("forward-button");
+ // We need to wait for the forward button transition to complete before we
+ // can click it, so we hook up a listener to wait for it to be ready.
+ let forwardTransitionPromise = BrowserTestUtils.waitForEvent(forwardButton, "transitionend");
+
+ let backPromise = BrowserTestUtils.browserStopped(aBrowser);
+ EventUtils.synthesizeMouseAtCenter(document.getElementById("back-button"), {});
+ yield backPromise;
+
+ // Give a chance for the doorhanger to appear
+ yield new Promise(resolve => SimpleTest.executeSoon(resolve));
+ ok(!getCaptureDoorhanger("any"), "No doorhanger should be present");
+
+ // Now go forward again after filling
+ yield fillTestPage(aBrowser);
+
+ yield forwardTransitionPromise;
+ info("transition done");
+ yield BrowserTestUtils.waitForCondition(() => {
+ return forwardButton.disabled == false;
+ });
+ let forwardPromise = BrowserTestUtils.browserStopped(aBrowser);
+ info("click the forward button");
+ EventUtils.synthesizeMouseAtCenter(forwardButton, {});
+ yield forwardPromise;
+ });
+});
+
+
+add_task(function* test_reloadButton() {
+ yield withTestPage(function*(aBrowser) {
+ let reloadButton = document.getElementById("urlbar-reload-button");
+ let loadPromise = BrowserTestUtils.browserLoaded(aBrowser, false,
+ "https://example.com" + DIRECTORY_PATH +
+ "formless_basic.html");
+
+ yield BrowserTestUtils.waitForCondition(() => {
+ return reloadButton.disabled == false;
+ });
+ EventUtils.synthesizeMouseAtCenter(reloadButton, {});
+ yield loadPromise;
+ });
+});
+
+add_task(function* test_back_keyboard_shortcut() {
+ if (Services.prefs.getIntPref("browser.backspace_action") != 0) {
+ ok(true, "Skipped testing backspace to go back since it's disabled");
+ return;
+ }
+ yield withTestPage(function*(aBrowser) {
+ // Load a new page in the tab so we can test going back
+ aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second");
+ yield BrowserTestUtils.browserLoaded(aBrowser, false,
+ "https://example.com" + DIRECTORY_PATH +
+ "formless_basic.html?second");
+ yield fillTestPage(aBrowser);
+
+ let backPromise = BrowserTestUtils.browserStopped(aBrowser);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+ yield backPromise;
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js
new file mode 100644
index 000000000..039312b7d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/LoginManagerParent.jsm", this);
+
+const testUrlPath =
+ "://example.com/browser/toolkit/components/passwordmgr/test/browser/";
+
+/**
+ * Waits for the given number of occurrences of InsecureLoginFormsStateChange
+ * on the given browser element.
+ */
+function waitForInsecureLoginFormsStateChange(browser, count) {
+ return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
+ false, () => --count == 0);
+}
+
+/**
+ * Checks that hasInsecureLoginForms is true for a simple HTTP page and false
+ * for a simple HTTPS page.
+ */
+add_task(function* test_simple() {
+ for (let scheme of ["http", "https"]) {
+ let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+
+ Assert.equal(LoginManagerParent.hasInsecureLoginForms(browser),
+ scheme == "http");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+/**
+ * Checks that hasInsecureLoginForms is true if a password field is present in
+ * an HTTP page loaded as a subframe of a top-level HTTPS page, when mixed
+ * active content blocking is disabled.
+ *
+ * When the subframe is navigated to an HTTPS page, hasInsecureLoginForms should
+ * be set to false.
+ *
+ * Moving back in history should set hasInsecureLoginForms to true again.
+ */
+add_task(function* test_subframe_navigation() {
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({
+ "set": [["security.mixed_content.block_active_content", false]],
+ }, resolve));
+
+ // Load the page with the subframe in a new tab.
+ let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // Two events are triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 3),
+ ]);
+
+ Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser));
+
+ // Navigate the subframe to a secure page.
+ let promiseSubframeReady = Promise.all([
+ BrowserTestUtils.browserLoaded(browser, true),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("test-iframe")
+ .contentDocument.getElementById("test-link").click();
+ });
+ yield promiseSubframeReady;
+
+ Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser));
+
+ // Navigate back to the insecure page. We only have to wait for the
+ // InsecureLoginFormsStateChange event that is triggered by pageshow.
+ let promise = waitForInsecureLoginFormsStateChange(browser, 1);
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("test-iframe")
+ .contentWindow.history.back();
+ });
+ yield promise;
+
+ Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser));
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js
new file mode 100644
index 000000000..2dbffb9cc
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/LoginManagerParent.jsm", this);
+
+function* registerConverter() {
+ Cu.import("resource://gre/modules/Services.jsm", this);
+ Cu.import("resource://gre/modules/NetUtil.jsm", this);
+
+ /**
+ * Converts the "test/content" MIME type, served by the test over HTTP, to an
+ * HTML viewer page containing the "form_basic.html" code. The viewer is
+ * served from a "resource:" URI while keeping the "resource:" principal.
+ */
+ function TestStreamConverter() {}
+
+ TestStreamConverter.prototype = {
+ classID: Components.ID("{5f01d6ef-c090-45a4-b3e5-940d64713eb7}"),
+ contractID: "@mozilla.org/streamconv;1?from=test/content&to=*/*",
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIRequestObserver,
+ Ci.nsIStreamListener,
+ Ci.nsIStreamConverter,
+ ]),
+
+ // nsIStreamConverter
+ convert() {},
+
+ // nsIStreamConverter
+ asyncConvertData(aFromType, aToType, aListener, aCtxt) {
+ this.listener = aListener;
+ },
+
+ // nsIRequestObserver
+ onStartRequest(aRequest, aContext) {
+ let channel = NetUtil.newChannel({
+ uri: "resource://testing-common/form_basic.html",
+ loadUsingSystemPrincipal: true,
+ });
+ channel.originalURI = aRequest.QueryInterface(Ci.nsIChannel).URI;
+ channel.loadGroup = aRequest.loadGroup;
+ channel.owner = Services.scriptSecurityManager
+ .createCodebasePrincipal(channel.URI, {});
+ // In this test, we pass the new channel to the listener but don't fire a
+ // redirect notification, even if it would be required. This keeps the
+ // test code simpler and doesn't impact the principal check we're testing.
+ channel.asyncOpen2(this.listener);
+ },
+
+ // nsIRequestObserver
+ onStopRequest() {},
+
+ // nsIStreamListener
+ onDataAvailable() {},
+ };
+
+ let factory = XPCOMUtils._getFactory(TestStreamConverter);
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(TestStreamConverter.prototype.classID, "",
+ TestStreamConverter.prototype.contractID, factory);
+ this.cleanupFunction = function () {
+ registrar.unregisterFactory(TestStreamConverter.prototype.classID, factory);
+ };
+}
+
+/**
+ * Waits for the given number of occurrences of InsecureLoginFormsStateChange
+ * on the given browser element.
+ */
+function waitForInsecureLoginFormsStateChange(browser, count) {
+ return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
+ false, () => --count == 0);
+}
+
+/**
+ * Checks that hasInsecureLoginForms is false for a viewer served internally
+ * using a "resource:" URI.
+ */
+add_task(function* test_streamConverter() {
+ let originalBrowser = gBrowser.selectedTab.linkedBrowser;
+
+ yield ContentTask.spawn(originalBrowser, null, registerConverter);
+
+ let tab = gBrowser.addTab("http://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/streamConverter_content.sjs",
+ { relatedBrowser: originalBrowser.linkedBrowser });
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+
+ Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser));
+
+ yield BrowserTestUtils.removeTab(tab);
+
+ yield ContentTask.spawn(originalBrowser, null, function* () {
+ this.cleanupFunction();
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js
new file mode 100644
index 000000000..beb928a34
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js
@@ -0,0 +1,78 @@
+const TEST_URL_PATH = "://example.org/browser/toolkit/components/passwordmgr/test/browser/";
+
+add_task(function* setup() {
+ let login = LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://example.org",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ login = LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://another.domain",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ yield SpecialPowers.pushPrefEnv({ "set": [["signon.autofillForms.http", false]] });
+});
+
+add_task(function* test_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic.html`);
+
+ let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let contentUsername = doc.getElementById("form-basic-username").value;
+ let contentPassword = doc.getElementById("form-basic-password").value;
+ return [contentUsername, contentPassword];
+ });
+
+ is(username, scheme == "http" ? "" : "username", "Username filled correctly");
+ is(password, scheme == "http" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(function* test_iframe_in_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic_iframe.html`);
+
+ let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let iframe = doc.getElementById("test-iframe");
+ let contentUsername = iframe.contentWindow.document.getElementById("form-basic-username").value;
+ let contentPassword = iframe.contentWindow.document.getElementById("form-basic-password").value;
+ return [contentUsername, contentPassword];
+ });
+
+ is(username, scheme == "http" ? "" : "username", "Username filled correctly");
+ is(password, scheme == "http" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(function* test_http_action_autofill() {
+ for (let type of ["insecure", "secure"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `https${TEST_URL_PATH}form_cross_origin_${type}_action.html`);
+
+ let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let contentUsername = doc.getElementById("form-basic-username").value;
+ let contentPassword = doc.getElementById("form-basic-password").value;
+ return [contentUsername, contentPassword];
+ });
+
+ is(username, type == "insecure" ? "" : "username", "Username filled correctly");
+ is(password, type == "insecure" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
diff --git a/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js
new file mode 100644
index 000000000..f16ae1b98
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js
@@ -0,0 +1,94 @@
+"use strict";
+
+const WARNING_PATTERN = [{
+ key: "INSECURE_FORM_ACTION",
+ msg: 'JavaScript Warning: "Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen."'
+}, {
+ key: "INSECURE_PAGE",
+ msg: 'JavaScript Warning: "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."'
+}];
+
+add_task(function* testInsecurePasswordWarning() {
+ let warningPatternHandler;
+
+ function messageHandler(msgObj) {
+ function findWarningPattern(msg) {
+ return WARNING_PATTERN.find(patternPair => {
+ return msg.indexOf(patternPair.msg) !== -1;
+ });
+ }
+
+ let warning = findWarningPattern(msgObj.message);
+
+ // Only handle the insecure password related warning messages.
+ if (warning) {
+ // Prevent any unexpected or redundant matched warning message coming after
+ // the test case is ended.
+ ok(warningPatternHandler, "Invoke a valid warning message handler");
+ warningPatternHandler(warning, msgObj.message);
+ }
+ }
+ Services.console.registerListener(messageHandler);
+ registerCleanupFunction(function() {
+ Services.console.unregisterListener(messageHandler);
+ });
+
+ for (let [origin, testFile, expectWarnings] of [
+ ["http://127.0.0.1", "form_basic.html", []],
+ ["http://127.0.0.1", "formless_basic.html", []],
+ ["http://example.com", "form_basic.html", ["INSECURE_PAGE"]],
+ ["http://example.com", "formless_basic.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_basic.html", []],
+ ["https://example.com", "formless_basic.html", []],
+
+ // For a form with customized action link in the same origin.
+ ["http://127.0.0.1", "form_same_origin_action.html", []],
+ ["http://example.com", "form_same_origin_action.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_same_origin_action.html", []],
+
+ // For a form with an insecure (http) customized action link.
+ ["http://127.0.0.1", "form_cross_origin_insecure_action.html", ["INSECURE_FORM_ACTION"]],
+ ["http://example.com", "form_cross_origin_insecure_action.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_cross_origin_insecure_action.html", ["INSECURE_FORM_ACTION"]],
+
+ // For a form with a secure (https) customized action link.
+ ["http://127.0.0.1", "form_cross_origin_secure_action.html", []],
+ ["http://example.com", "form_cross_origin_secure_action.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_cross_origin_secure_action.html", []],
+ ]) {
+ let testURL = origin + DIRECTORY_PATH + testFile;
+ let promiseConsoleMessages = new Promise(resolve => {
+ warningPatternHandler = function (warning, originMessage) {
+ ok(warning, "Handling a warning pattern");
+ let fullMessage = `[${warning.msg} {file: "${testURL}" line: 0 column: 0 source: "0"}]`;
+ is(originMessage, fullMessage, "Message full matched:" + originMessage);
+
+ let index = expectWarnings.indexOf(warning.key);
+ isnot(index, -1, "Found warning: " + warning.key + " for URL:" + testURL);
+ if (index !== -1) {
+ // Remove the shown message.
+ expectWarnings.splice(index, 1);
+ }
+ if (expectWarnings.length === 0) {
+ info("All warnings are shown for URL:" + testURL);
+ resolve();
+ }
+ };
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: testURL
+ }, function*() {
+ if (expectWarnings.length === 0) {
+ info("All warnings are shown for URL:" + testURL);
+ return Promise.resolve();
+ }
+ return promiseConsoleMessages;
+ });
+
+ // Remove warningPatternHandler to stop handling the matched warning pattern
+ // and the task should not get any warning anymore.
+ warningPatternHandler = null;
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js
new file mode 100644
index 000000000..f3bc62b0a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js
@@ -0,0 +1,59 @@
+const HOST = "https://example.com";
+const URL = HOST + "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
+const TIMEOUT_PREF = "signon.masterPasswordReprompt.timeout_ms";
+
+// Waits for the master password prompt and cancels it.
+function waitForDialog() {
+ let dialogShown = TestUtils.topicObserved("common-dialog-loaded");
+ return dialogShown.then(function([subject]) {
+ let dialog = subject.Dialog;
+ is(dialog.args.title, "Password Required");
+ dialog.ui.button1.click();
+ });
+}
+
+// Test that autocomplete does not trigger a master password prompt
+// for a certain time after it was cancelled.
+add_task(function* test_mpAutocompleteTimeout() {
+ let login = LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ LoginTestUtils.masterPassword.enable();
+
+ registerCleanupFunction(function() {
+ LoginTestUtils.masterPassword.disable();
+ Services.logins.removeAllLogins();
+ });
+
+ // Set master password prompt timeout to 3s.
+ // If this test goes intermittent, you likely have to increase this value.
+ yield SpecialPowers.pushPrefEnv({set: [[TIMEOUT_PREF, 3000]]});
+
+ // Wait for initial master password dialog after opening the tab.
+ let dialogShown = waitForDialog();
+
+ yield BrowserTestUtils.withNewTab(URL, function*(browser) {
+ yield dialogShown;
+
+ yield ContentTask.spawn(browser, null, function*() {
+ // Focus the password field to trigger autocompletion.
+ content.document.getElementById("form-basic-password").focus();
+ });
+
+ // Wait 4s, dialog should not have been shown
+ // (otherwise the code below will not work).
+ yield new Promise((c) => setTimeout(c, 4000));
+
+ dialogShown = waitForDialog();
+ yield ContentTask.spawn(browser, null, function*() {
+ // Re-focus the password field to trigger autocompletion.
+ content.document.getElementById("form-basic-username").focus();
+ content.document.getElementById("form-basic-password").focus();
+ });
+ yield dialogShown;
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications.js b/toolkit/components/passwordmgr/test/browser/browser_notifications.js
new file mode 100644
index 000000000..4fb012f14
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications.js
@@ -0,0 +1,81 @@
+/**
+ * Test that the doorhanger notification for password saving is populated with
+ * the correct values in various password capture cases.
+ */
+add_task(function* test_save_change() {
+ let testCases = [{
+ username: "username",
+ password: "password",
+ }, {
+ username: "",
+ password: "password",
+ }, {
+ username: "username",
+ oldPassword: "password",
+ password: "newPassword",
+ }, {
+ username: "",
+ oldPassword: "password",
+ password: "newPassword",
+ }];
+
+ for (let { username, oldPassword, password } of testCases) {
+ // Add a login for the origin of the form if testing a change notification.
+ if (oldPassword) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username,
+ password: oldPassword,
+ }));
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, [username, password],
+ function* ([contentUsername, contentPassword]) {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = contentUsername;
+ doc.getElementById("form-basic-password").value = contentPassword;
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ // Style flush to make sure binding is attached
+ notificationElement.querySelector("#password-notification-password").clientTop;
+
+ // Check the actual content of the popup notification.
+ Assert.equal(notificationElement.querySelector("#password-notification-username")
+ .value, username);
+ Assert.equal(notificationElement.querySelector("#password-notification-password")
+ .value, password);
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = oldPassword ? "modifyLogin" : "addLogin";
+ let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification);
+ notificationElement.button.doCommand();
+ let [result] = yield promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = oldPassword ? result.QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+ Assert.equal(login.username, username);
+ Assert.equal(login.password, password);
+ });
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllLogins();
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
new file mode 100644
index 000000000..48c73b0e6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
@@ -0,0 +1,125 @@
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["signon.rememberSignons.visibilityToggle", true]
+ ]});
+});
+
+/**
+ * Test that the doorhanger main action button is disabled
+ * when the password field is empty.
+ *
+ * Also checks that submiting an empty password throws an error.
+ */
+add_task(function* test_empty_password() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, null,
+ function* () {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = "username";
+ doc.getElementById("form-basic-password").value = "p";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+ let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+ // Synthesize input to empty the field
+ passwordTextbox.focus();
+ yield EventUtils.synthesizeKey("VK_RIGHT", {});
+ yield EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+
+ let mainActionButton = document.getAnonymousElementByAttribute(notificationElement.button, "anonid", "button");
+ Assert.ok(mainActionButton.disabled, "Main action button is disabled");
+
+ // Makes sure submiting an empty password throws an error
+ Assert.throws(notificationElement.button.doCommand(),
+ "Can't add a login with a null or empty password.",
+ "Should fail for an empty password");
+ });
+});
+
+/**
+ * Test that the doorhanger password field shows plain or * text
+ * when the checkbox is checked.
+ */
+add_task(function* test_toggle_password() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, null,
+ function* () {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = "username";
+ doc.getElementById("form-basic-password").value = "p";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+ let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+ yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+ Assert.ok(toggleCheckbox.checked);
+ Assert.equal(passwordTextbox.type, "", "Password textbox changed to plain text");
+
+ yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+ Assert.ok(!toggleCheckbox.checked);
+ Assert.equal(passwordTextbox.type, "password", "Password textbox changed to * text");
+ });
+});
+
+/**
+ * Test that the doorhanger password toggle checkbox is disabled
+ * when the master password is set.
+ */
+add_task(function* test_checkbox_disabled_if_has_master_password() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+
+ LoginTestUtils.masterPassword.enable();
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = "username";
+ doc.getElementById("form-basic-password").value = "p";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+ let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+ Assert.equal(passwordTextbox.type, "password", "Password textbox should show * text");
+ Assert.ok(toggleCheckbox.getAttribute("hidden"), "checkbox is hidden when master password is set");
+ });
+
+ LoginTestUtils.masterPassword.disable();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js
new file mode 100644
index 000000000..8ac49dac5
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js
@@ -0,0 +1,145 @@
+/**
+ * Test changing the password inside the doorhanger notification for passwords.
+ *
+ * We check the following cases:
+ * - Editing the password of a new login.
+ * - Editing the password of an existing login.
+ * - Changing both username and password to an existing login.
+ * - Changing the username to an existing login.
+ * - Editing username to an empty one and a new password.
+ *
+ * If both the username and password matches an already existing login, we should not
+ * update it's password, but only it's usage timestamp and count.
+ */
+add_task(function* test_edit_password() {
+ let testCases = [{
+ usernameInPage: "username",
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 1,
+ }, {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ passwordInPage: "password",
+ passwordInStorage: "oldPassword",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ }, {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ }, {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "password",
+ timesUsed: 2,
+ checkPasswordNotUpdated: true,
+ }, {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ }];
+
+ for (let testCase of testCases) {
+ info("Test case: " + JSON.stringify(testCase));
+
+ // Create the pre-existing logins when needed.
+ if (testCase.usernameInPageExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameInPage,
+ password: testCase.passwordInStorage,
+ }));
+ }
+
+ if (testCase.usernameChangedToExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameChangedTo,
+ password: testCase.passwordChangedTo,
+ }));
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, testCase,
+ function* (contentTestCase) {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = contentTestCase.usernameInPage;
+ doc.getElementById("form-basic-password").value = contentTestCase.passwordInPage;
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ // Style flush to make sure binding is attached
+ notificationElement.querySelector("#password-notification-password").clientTop;
+
+ // Modify the username in the dialog if requested.
+ if (testCase.usernameChangedTo) {
+ notificationElement.querySelector("#password-notification-username")
+ .value = testCase.usernameChangedTo;
+ }
+
+ // Modify the password in the dialog if requested.
+ if (testCase.passwordChangedTo) {
+ notificationElement.querySelector("#password-notification-password")
+ .value = testCase.passwordChangedTo;
+ }
+
+ // We expect a modifyLogin notification if the final username used by the
+ // dialog exists in the logins database, otherwise an addLogin one.
+ let expectModifyLogin = typeof testCase.usernameChangedTo !== "undefined"
+ ? testCase.usernameChangedToExists
+ : testCase.usernameInPageExists;
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin";
+ let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification);
+ notificationElement.button.doCommand();
+ let [result] = yield promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+
+ Assert.equal(login.username, testCase.usernameChangedTo ||
+ testCase.usernameInPage);
+ Assert.equal(login.password, testCase.passwordChangedTo ||
+ testCase.passwordInPage);
+
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(meta.timesUsed, testCase.timesUsed);
+
+ // Check that the password was not updated if the user is empty
+ if (testCase.checkPasswordNotUpdated) {
+ Assert.ok(meta.timeLastUsed > meta.timeCreated);
+ Assert.ok(meta.timeCreated == meta.timePasswordChanged);
+ }
+ });
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllLogins();
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js
new file mode 100644
index 000000000..2c9ea2607
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js
@@ -0,0 +1,119 @@
+/**
+ * Test changing the username inside the doorhanger notification for passwords.
+ *
+ * We have to test combination of existing and non-existing logins both for
+ * the original one from the webpage and the final one used by the dialog.
+ *
+ * We also check switching to and from empty usernames.
+ */
+add_task(function* test_edit_username() {
+ let testCases = [{
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ }, {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ usernameChangedTo: "newUsername",
+ }, {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ }, {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ }, {
+ usernameInPage: "",
+ usernameChangedTo: "newUsername",
+ }, {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ }, {
+ usernameInPage: "",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ }, {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ usernameChangedToExists: true,
+ }];
+
+ for (let testCase of testCases) {
+ info("Test case: " + JSON.stringify(testCase));
+
+ // Create the pre-existing logins when needed.
+ if (testCase.usernameInPageExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameInPage,
+ password: "old password",
+ }));
+ }
+
+ if (testCase.usernameChangedToExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameChangedTo,
+ password: "old password",
+ }));
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, testCase.usernameInPage,
+ function* (usernameInPage) {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = usernameInPage;
+ doc.getElementById("form-basic-password").value = "password";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ // Style flush to make sure binding is attached
+ notificationElement.querySelector("#password-notification-password").clientTop;
+
+ // Modify the username in the dialog if requested.
+ if (testCase.usernameChangedTo) {
+ notificationElement.querySelector("#password-notification-username")
+ .value = testCase.usernameChangedTo;
+ }
+
+ // We expect a modifyLogin notification if the final username used by the
+ // dialog exists in the logins database, otherwise an addLogin one.
+ let expectModifyLogin = testCase.usernameChangedTo
+ ? testCase.usernameChangedToExists
+ : testCase.usernameInPageExists;
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin";
+ let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification);
+ notificationElement.button.doCommand();
+ let [result] = yield promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+ Assert.equal(login.username, testCase.usernameChangedTo ||
+ testCase.usernameInPage);
+ Assert.equal(login.password, "password");
+ });
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllLogins();
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js
new file mode 100644
index 000000000..ece2b731f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.logins.removeAllLogins();
+
+ // Add some initial logins
+ let urls = [
+ "http://example.com/",
+ "http://mozilla.org/",
+ "http://spreadfirefox.com/",
+ "https://support.mozilla.org/",
+ "http://hg.mozilla.org/"
+ ];
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let logins = [
+ new nsLoginInfo(urls[0], urls[0], null, "", "o hai", "u1", "p1"),
+ new nsLoginInfo(urls[1], urls[1], null, "ehsan", "coded", "u2", "p2"),
+ new nsLoginInfo(urls[2], urls[2], null, "this", "awesome", "u3", "p3"),
+ new nsLoginInfo(urls[3], urls[3], null, "array of", "logins", "u4", "p4"),
+ new nsLoginInfo(urls[4], urls[4], null, "then", "i wrote the test", "u5", "p5")
+ ];
+ logins.forEach(login => Services.logins.addLogin(login));
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ // Test if "Copy Username" and "Copy Password" works
+ function doTest() {
+ let doc = pwmgrdlg.document;
+ let selection = doc.getElementById("signonsTree").view.selection;
+ let menuitem = doc.getElementById("context-copyusername");
+
+ function copyField() {
+ info("Select all");
+ selection.selectAll();
+ assertMenuitemEnabled("copyusername", false);
+ assertMenuitemEnabled("editusername", false);
+ assertMenuitemEnabled("copypassword", false);
+ assertMenuitemEnabled("editpassword", false);
+
+ info("Select the first row (with an empty username)");
+ selection.select(0);
+ assertMenuitemEnabled("copyusername", false, "empty username");
+ assertMenuitemEnabled("editusername", true);
+ assertMenuitemEnabled("copypassword", true);
+ assertMenuitemEnabled("editpassword", false, "password column hidden");
+
+ info("Clear the selection");
+ selection.clearSelection();
+ assertMenuitemEnabled("copyusername", false);
+ assertMenuitemEnabled("editusername", false);
+ assertMenuitemEnabled("copypassword", false);
+ assertMenuitemEnabled("editpassword", false);
+
+ info("Select the third row and making the password column visible");
+ selection.select(2);
+ doc.getElementById("passwordCol").hidden = false;
+ assertMenuitemEnabled("copyusername", true);
+ assertMenuitemEnabled("editusername", true);
+ assertMenuitemEnabled("copypassword", true);
+ assertMenuitemEnabled("editpassword", true, "password column visible");
+ menuitem.doCommand();
+ }
+
+ function assertMenuitemEnabled(idSuffix, expected, reason = "") {
+ doc.defaultView.UpdateContextMenu();
+ let actual = !doc.getElementById("context-" + idSuffix).getAttribute("disabled");
+ is(actual, expected, idSuffix + " should be " + (expected ? "enabled" : "disabled") +
+ (reason ? ": " + reason : ""));
+ }
+
+ function cleanUp() {
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ Services.ww.unregisterNotification(arguments.callee);
+ Services.logins.removeAllLogins();
+ doc.getElementById("passwordCol").hidden = true;
+ finish();
+ });
+ pwmgrdlg.close();
+ }
+
+ function testPassword() {
+ info("Testing Copy Password");
+ waitForClipboard("coded", function copyPassword() {
+ menuitem = doc.getElementById("context-copypassword");
+ menuitem.doCommand();
+ }, cleanUp, cleanUp);
+ }
+
+ info("Testing Copy Username");
+ waitForClipboard("ehsan", copyField, testPassword, testPassword);
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
new file mode 100644
index 000000000..2b2e42273
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
@@ -0,0 +1,126 @@
+const { ContentTaskUtils } = Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
+const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+
+var doc;
+var pwmgr;
+var pwmgrdlg;
+var signonsTree;
+
+function addLogin(site, username, password) {
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let login = new nsLoginInfo(site, site, null, username, password, "u", "p");
+ Services.logins.addLogin(login);
+}
+
+function getUsername(row) {
+ return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("userCol"));
+}
+
+function getPassword(row) {
+ return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("passwordCol"));
+}
+
+function synthesizeDblClickOnCell(aTree, column, row) {
+ let tbo = aTree.treeBoxObject;
+ let rect = tbo.getCoordsForCellItem(row, aTree.columns[column], "text");
+ let x = rect.x + rect.width / 2;
+ let y = rect.y + rect.height / 2;
+ // Simulate the double click.
+ EventUtils.synthesizeMouse(aTree.body, x, y, { clickCount: 2 },
+ aTree.ownerDocument.defaultView);
+}
+
+function* togglePasswords() {
+ pwmgrdlg.document.querySelector("#togglePasswords").doCommand();
+ yield new Promise(resolve => waitForFocus(resolve, pwmgrdlg));
+}
+
+function* editUsernamePromises(site, oldUsername, newUsername) {
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
+ let login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.username, oldUsername, "Correct username saved");
+ is(getUsername(0), oldUsername, "Correct username shown");
+ synthesizeDblClickOnCell(signonsTree, 1, 0);
+ yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"),
+ "Waiting for editing");
+
+ EventUtils.sendString(newUsername, pwmgrdlg);
+ let signonsIntro = doc.querySelector("#signonsIntro");
+ EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
+ yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
+ "Waiting for editing to stop");
+
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
+ login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.username, newUsername, "Correct username updated");
+ is(getUsername(0), newUsername, "Correct username shown after the update");
+}
+
+function* editPasswordPromises(site, oldPassword, newPassword) {
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
+ let login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.password, oldPassword, "Correct password saved");
+ is(getPassword(0), oldPassword, "Correct password shown");
+
+ synthesizeDblClickOnCell(signonsTree, 2, 0);
+ yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"),
+ "Waiting for editing");
+
+ EventUtils.sendString(newPassword, pwmgrdlg);
+ let signonsIntro = doc.querySelector("#signonsIntro");
+ EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
+ yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
+ "Waiting for editing to stop");
+
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
+ login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.password, newPassword, "Correct password updated");
+ is(getPassword(0), newPassword, "Correct password shown after the update");
+}
+
+add_task(function* test_setup() {
+ registerCleanupFunction(function() {
+ Services.logins.removeAllLogins();
+ });
+
+ Services.logins.removeAllLogins();
+ // Open the password manager dialog.
+ pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ SimpleTest.waitForFocus(function() {
+ EventUtils.sendKey("RETURN", win);
+ }, win);
+ } else if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") {
+ // Unregister ourself.
+ Services.ww.unregisterNotification(arguments.callee);
+ }
+ });
+
+ yield new Promise((resolve) => {
+ SimpleTest.waitForFocus(() => {
+ doc = pwmgrdlg.document;
+ signonsTree = doc.querySelector("#signonsTree");
+ resolve();
+ }, pwmgrdlg);
+ });
+});
+
+add_task(function* test_edit_multiple_logins() {
+ function* testLoginChange(site, oldUsername, oldPassword, newUsername, newPassword) {
+ addLogin(site, oldUsername, oldPassword);
+ yield* editUsernamePromises(site, oldUsername, newUsername);
+ yield* togglePasswords();
+ yield* editPasswordPromises(site, oldPassword, newPassword);
+ yield* togglePasswords();
+ }
+
+ yield* testLoginChange("http://c.tn/", "userC", "passC", "usernameC", "passwordC");
+ yield* testLoginChange("http://b.tn/", "userB", "passB", "usernameB", "passwordB");
+ yield* testLoginChange("http://a.tn/", "userA", "passA", "usernameA", "passwordA");
+
+ pwmgrdlg.close();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js
new file mode 100644
index 000000000..95bcee9ed
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ pwmgr.removeAllLogins();
+
+ // add login data
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let login = new nsLoginInfo("http://example.com/", "http://example.com/", null,
+ "user", "password", "u1", "p1");
+ pwmgr.addLogin(login);
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ function doTest() {
+ let doc = pwmgrdlg.document;
+
+ let signonsTree = doc.querySelector("#signonsTree");
+ is(signonsTree.view.rowCount, 1, "One entry in the passwords list");
+
+ is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("siteCol")),
+ "http://example.com/",
+ "Correct website saved");
+
+ is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("userCol")),
+ "user",
+ "Correct user saved");
+
+ let timeCreatedCol = doc.getElementById("timeCreatedCol");
+ is(timeCreatedCol.getAttribute("hidden"), "true",
+ "Time created column is not displayed");
+
+
+ let timeLastUsedCol = doc.getElementById("timeLastUsedCol");
+ is(timeLastUsedCol.getAttribute("hidden"), "true",
+ "Last Used column is not displayed");
+
+ let timePasswordChangedCol = doc.getElementById("timePasswordChangedCol");
+ is(timePasswordChangedCol.getAttribute("hidden"), "",
+ "Last Changed column is displayed");
+
+ // cleanup
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") {
+ // unregister ourself
+ Services.ww.unregisterNotification(arguments.callee);
+
+ pwmgr.removeAllLogins();
+
+ finish();
+ }
+ });
+
+ pwmgrdlg.close();
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js
new file mode 100644
index 000000000..1dc7076aa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ const LOGIN_HOST = "http://example.com";
+ const LOGIN_COUNT = 5;
+
+ let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
+ let pmDialog = window.openDialog(
+ "chrome://passwordmgr/content/passwordManager.xul",
+ "Toolkit:PasswordManager", "");
+
+ let logins = [];
+ let loginCounter = 0;
+ let loginOrder = null;
+ let modifiedLogin;
+ let testNumber = 0;
+ let testObserver = {
+ observe: function (subject, topic, data) {
+ if (topic == "passwordmgr-dialog-updated") {
+ switch (testNumber) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ is(countLogins(), loginCounter, "Verify login added");
+ ok(getLoginOrder().startsWith(loginOrder), "Verify login order");
+ runNextTest();
+ break;
+ case 6:
+ is(countLogins(), loginCounter, "Verify login count");
+ is(getLoginOrder(), loginOrder, "Verify login order");
+ is(getLoginPassword(), "newpassword0", "Verify login modified");
+ runNextTest();
+ break;
+ case 7:
+ is(countLogins(), loginCounter, "Verify login removed");
+ ok(loginOrder.endsWith(getLoginOrder()), "Verify login order");
+ runNextTest();
+ break;
+ case 8:
+ is(countLogins(), 0, "Verify all logins removed");
+ runNextTest();
+ break;
+ }
+ }
+ }
+ };
+
+ SimpleTest.waitForFocus(startTest, pmDialog);
+
+ function createLogins() {
+ let login;
+ for (let i = 0; i < LOGIN_COUNT; i++) {
+ login = new nsLoginInfo(LOGIN_HOST + "?n=" + i, LOGIN_HOST + "?n=" + i,
+ null, "user" + i, "password" + i, "u" + i, "p" + i);
+ logins.push(login);
+ }
+ modifiedLogin = new nsLoginInfo(LOGIN_HOST + "?n=0", LOGIN_HOST + "?n=0",
+ null, "user0", "newpassword0", "u0", "p0");
+ is(logins.length, LOGIN_COUNT, "Verify logins created");
+ }
+
+ function countLogins() {
+ let doc = pmDialog.document;
+ let signonsTree = doc.getElementById("signonsTree");
+ return signonsTree.view.rowCount;
+ }
+
+ function getLoginOrder() {
+ let doc = pmDialog.document;
+ let signonsTree = doc.getElementById("signonsTree");
+ let column = signonsTree.columns[0]; // host column
+ let order = [];
+ for (let i = 0; i < signonsTree.view.rowCount; i++) {
+ order.push(signonsTree.view.getCellText(i, column));
+ }
+ return order.join(',');
+ }
+
+ function getLoginPassword() {
+ let doc = pmDialog.document;
+ let loginsTree = doc.getElementById("signonsTree");
+ let column = loginsTree.columns[2]; // password column
+ return loginsTree.view.getCellText(0, column);
+ }
+
+ function startTest() {
+ Services.obs.addObserver(
+ testObserver, "passwordmgr-dialog-updated", false);
+ is(countLogins(), 0, "Verify starts with 0 logins");
+ createLogins();
+ runNextTest();
+ }
+
+ function runNextTest() {
+ switch (++testNumber) {
+ case 1: // add the logins
+ for (let i = 0; i < logins.length; i++) {
+ loginCounter++;
+ loginOrder = getLoginOrder();
+ Services.logins.addLogin(logins[i]);
+ }
+ break;
+ case 6: // modify a login
+ loginOrder = getLoginOrder();
+ Services.logins.modifyLogin(logins[0], modifiedLogin);
+ break;
+ case 7: // remove a login
+ loginCounter--;
+ loginOrder = getLoginOrder();
+ Services.logins.removeLogin(modifiedLogin);
+ break;
+ case 8: // remove all logins
+ Services.logins.removeAllLogins();
+ break;
+ case 9: // finish
+ Services.obs.removeObserver(
+ testObserver, "passwordmgr-dialog-updated", false);
+ pmDialog.close();
+ finish();
+ break;
+ }
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js
new file mode 100644
index 000000000..83272a9c4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ pwmgr.removeAllLogins();
+
+ // Add some initial logins
+ let urls = [
+ "http://example.com/",
+ "http://example.org/",
+ "http://mozilla.com/",
+ "http://mozilla.org/",
+ "http://spreadfirefox.com/",
+ "http://planet.mozilla.org/",
+ "https://developer.mozilla.org/",
+ "http://hg.mozilla.org/",
+ "http://dxr.mozilla.org/",
+ "http://feeds.mozilla.org/",
+ ];
+ let users = [
+ "user",
+ "username",
+ "ehsan",
+ "ehsan",
+ "john",
+ "what?",
+ "really?",
+ "you sure?",
+ "my user name",
+ "my username",
+ ];
+ let pwds = [
+ "password",
+ "password",
+ "mypass",
+ "mypass",
+ "smith",
+ "very secret",
+ "super secret",
+ "absolutely",
+ "mozilla",
+ "mozilla.com",
+ ];
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ for (let i = 0; i < 10; i++)
+ pwmgr.addLogin(new nsLoginInfo(urls[i], urls[i], null, users[i], pwds[i],
+ "u" + (i + 1), "p" + (i + 1)));
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ // the meat of the test
+ function doTest() {
+ let doc = pwmgrdlg.document;
+ let win = doc.defaultView;
+ let sTree = doc.getElementById("signonsTree");
+ let filter = doc.getElementById("filter");
+ let siteCol = doc.getElementById("siteCol");
+ let userCol = doc.getElementById("userCol");
+ let passwordCol = doc.getElementById("passwordCol");
+
+ let toggleCalls = 0;
+ function toggleShowPasswords(func) {
+ let toggleButton = doc.getElementById("togglePasswords");
+ let showMode = (toggleCalls++ % 2) == 0;
+
+ // only watch for a confirmation dialog every other time being called
+ if (showMode) {
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowclosed")
+ Services.ww.unregisterNotification(arguments.callee);
+ else if (aTopic == "domwindowopened") {
+ let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ SimpleTest.waitForFocus(function() {
+ EventUtils.sendKey("RETURN", targetWin);
+ }, targetWin);
+ }
+ });
+ }
+
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ if (aTopic == "passwordmgr-password-toggle-complete") {
+ Services.obs.removeObserver(arguments.callee, aTopic);
+ func();
+ }
+ }, "passwordmgr-password-toggle-complete", false);
+
+ EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win);
+ }
+
+ function clickCol(col) {
+ EventUtils.synthesizeMouse(col, 20, 1, {}, win);
+ setTimeout(runNextTest, 0);
+ }
+
+ function setFilter(string) {
+ filter.value = string;
+ filter.doCommand();
+ setTimeout(runNextTest, 0);
+ }
+
+ function checkSortMarkers(activeCol) {
+ let isOk = true;
+ let col = null;
+ let hasAttr = false;
+ let treecols = activeCol.parentNode;
+ for (let i = 0; i < treecols.childNodes.length; i++) {
+ col = treecols.childNodes[i];
+ if (col.nodeName != "treecol")
+ continue;
+ hasAttr = col.hasAttribute("sortDirection");
+ isOk &= col == activeCol ? hasAttr : !hasAttr;
+ }
+ ok(isOk, "Only " + activeCol.id + " has a sort marker");
+ }
+
+ function checkSortDirection(col, ascending) {
+ checkSortMarkers(col);
+ let direction = ascending ? "ascending" : "descending";
+ is(col.getAttribute("sortDirection"), direction,
+ col.id + ": sort direction is " + direction);
+ }
+
+ function checkColumnEntries(aCol, expectedValues) {
+ let actualValues = getColumnEntries(aCol);
+ is(actualValues.length, expectedValues.length, "Checking length of expected column");
+ for (let i = 0; i < expectedValues.length; i++)
+ is(actualValues[i], expectedValues[i], "Checking column entry #" + i);
+ }
+
+ function getColumnEntries(aCol) {
+ let entries = [];
+ let column = sTree.columns[aCol];
+ let numRows = sTree.view.rowCount;
+ for (let i = 0; i < numRows; i++)
+ entries.push(sTree.view.getCellText(i, column));
+ return entries;
+ }
+
+ let testCounter = 0;
+ let expectedValues;
+ function runNextTest() {
+ switch (testCounter++) {
+ case 0:
+ expectedValues = urls.slice().sort();
+ checkColumnEntries(0, expectedValues);
+ checkSortDirection(siteCol, true);
+ // Toggle sort direction on Host column
+ clickCol(siteCol);
+ break;
+ case 1:
+ expectedValues.reverse();
+ checkColumnEntries(0, expectedValues);
+ checkSortDirection(siteCol, false);
+ // Sort by Username
+ clickCol(userCol);
+ break;
+ case 2:
+ expectedValues = users.slice().sort();
+ checkColumnEntries(1, expectedValues);
+ checkSortDirection(userCol, true);
+ // Sort by Password
+ clickCol(passwordCol);
+ break;
+ case 3:
+ expectedValues = pwds.slice().sort();
+ checkColumnEntries(2, expectedValues);
+ checkSortDirection(passwordCol, true);
+ // Set filter
+ setFilter("moz");
+ break;
+ case 4:
+ expectedValues = [ "absolutely", "mozilla", "mozilla.com",
+ "mypass", "mypass", "super secret",
+ "very secret" ];
+ checkColumnEntries(2, expectedValues);
+ checkSortDirection(passwordCol, true);
+ // Reset filter
+ setFilter("");
+ break;
+ case 5:
+ expectedValues = pwds.slice().sort();
+ checkColumnEntries(2, expectedValues);
+ checkSortDirection(passwordCol, true);
+ // cleanup
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ // unregister ourself
+ Services.ww.unregisterNotification(arguments.callee);
+
+ pwmgr.removeAllLogins();
+ finish();
+ });
+ pwmgrdlg.close();
+ }
+ }
+
+ // Toggle Show Passwords to display Password column, then start tests
+ toggleShowPasswords(runNextTest);
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js
new file mode 100644
index 000000000..bd4f265b5
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PROMPT_URL = "chrome://global/content/commonDialog.xul";
+var { interfaces: Ci } = Components;
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.addTab();
+ isnot(tab, gBrowser.selectedTab, "New tab shouldn't be selected");
+
+ let listener = {
+ onOpenWindow: function(window) {
+ var domwindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ waitForFocus(() => {
+ is(domwindow.document.location.href, PROMPT_URL, "Should have seen a prompt window");
+ is(domwindow.args.promptType, "promptUserAndPass", "Should be an authenticate prompt");
+
+ is(gBrowser.selectedTab, tab, "Should have selected the new tab");
+
+ domwindow.document.documentElement.cancelDialog();
+ }, domwindow);
+ },
+
+ onCloseWindow: function() {
+ }
+ };
+
+ Services.wm.addListener(listener);
+ registerCleanupFunction(() => {
+ Services.wm.removeListener(listener);
+ gBrowser.removeTab(tab);
+ });
+
+ tab.linkedBrowser.addEventListener("load", () => {
+ finish();
+ }, true);
+ tab.linkedBrowser.loadURI("http://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs");
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js
new file mode 100644
index 000000000..57cfa9f83
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ pwmgr.removeAllLogins();
+
+ // Add some initial logins
+ let urls = [
+ "http://example.com/",
+ "http://example.org/",
+ "http://mozilla.com/",
+ "http://mozilla.org/",
+ "http://spreadfirefox.com/",
+ "http://planet.mozilla.org/",
+ "https://developer.mozilla.org/",
+ "http://hg.mozilla.org/",
+ "http://dxr.mozilla.org/",
+ "http://feeds.mozilla.org/",
+ ];
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let logins = [
+ new nsLoginInfo(urls[0], urls[0], null, "user", "password", "u1", "p1"),
+ new nsLoginInfo(urls[1], urls[1], null, "username", "password", "u2", "p2"),
+ new nsLoginInfo(urls[2], urls[2], null, "ehsan", "mypass", "u3", "p3"),
+ new nsLoginInfo(urls[3], urls[3], null, "ehsan", "mypass", "u4", "p4"),
+ new nsLoginInfo(urls[4], urls[4], null, "john", "smith", "u5", "p5"),
+ new nsLoginInfo(urls[5], urls[5], null, "what?", "very secret", "u6", "p6"),
+ new nsLoginInfo(urls[6], urls[6], null, "really?", "super secret", "u7", "p7"),
+ new nsLoginInfo(urls[7], urls[7], null, "you sure?", "absolutely", "u8", "p8"),
+ new nsLoginInfo(urls[8], urls[8], null, "my user name", "mozilla", "u9", "p9"),
+ new nsLoginInfo(urls[9], urls[9], null, "my username", "mozilla.com", "u10", "p10"),
+ ];
+ logins.forEach(login => pwmgr.addLogin(login));
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ // the meat of the test
+ function doTest() {
+ let doc = pwmgrdlg.document;
+ let win = doc.defaultView;
+ let filter = doc.getElementById("filter");
+ let tree = doc.getElementById("signonsTree");
+ let view = tree.view;
+
+ is(filter.value, "", "Filter box should initially be empty");
+ is(view.rowCount, 10, "There should be 10 passwords initially");
+
+ // Prepare a set of tests
+ // filter: the text entered in the filter search box
+ // count: the number of logins which should match the respective filter
+ // count2: the number of logins which should match the respective filter
+ // if the passwords are being shown as well
+ // Note: if a test doesn't have count2 set, count is used instead.
+ let tests = [
+ {filter: "pass", count: 0, count2: 4},
+ {filter: "", count: 10}, // test clearing the filter
+ {filter: "moz", count: 7},
+ {filter: "mozi", count: 7},
+ {filter: "mozil", count: 7},
+ {filter: "mozill", count: 7},
+ {filter: "mozilla", count: 7},
+ {filter: "mozilla.com", count: 1, count2: 2},
+ {filter: "user", count: 4},
+ {filter: "user ", count: 1},
+ {filter: " user", count: 2},
+ {filter: "http", count: 10},
+ {filter: "https", count: 1},
+ {filter: "secret", count: 0, count2: 2},
+ {filter: "secret!", count: 0},
+ ];
+
+ let toggleCalls = 0;
+ function toggleShowPasswords(func) {
+ let toggleButton = doc.getElementById("togglePasswords");
+ let showMode = (toggleCalls++ % 2) == 0;
+
+ // only watch for a confirmation dialog every other time being called
+ if (showMode) {
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowclosed")
+ Services.ww.unregisterNotification(arguments.callee);
+ else if (aTopic == "domwindowopened") {
+ let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ SimpleTest.waitForFocus(function() {
+ EventUtils.sendKey("RETURN", targetWin);
+ }, targetWin);
+ }
+ });
+ }
+
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ if (aTopic == "passwordmgr-password-toggle-complete") {
+ Services.obs.removeObserver(arguments.callee, aTopic);
+ func();
+ }
+ }, "passwordmgr-password-toggle-complete", false);
+
+ EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win);
+ }
+
+ function runTests(mode, endFunction) {
+ let testCounter = 0;
+
+ function setFilter(string) {
+ filter.value = string;
+ filter.doCommand();
+ }
+
+ function runOneTest(testCase) {
+ function tester() {
+ is(view.rowCount, expected, expected + " logins should match '" + testCase.filter + "'");
+ }
+
+ let expected;
+ switch (mode) {
+ case 1: // without showing passwords
+ expected = testCase.count;
+ break;
+ case 2: // showing passwords
+ expected = ("count2" in testCase) ? testCase.count2 : testCase.count;
+ break;
+ case 3: // toggle
+ expected = testCase.count;
+ tester();
+ toggleShowPasswords(function () {
+ expected = ("count2" in testCase) ? testCase.count2 : testCase.count;
+ tester();
+ toggleShowPasswords(proceed);
+ });
+ return;
+ }
+ tester();
+ proceed();
+ }
+
+ function proceed() {
+ // run the next test if necessary or proceed with the tests
+ if (testCounter != tests.length)
+ runNextTest();
+ else
+ endFunction();
+ }
+
+ function runNextTest() {
+ let testCase = tests[testCounter++];
+ setFilter(testCase.filter);
+ setTimeout(runOneTest, 0, testCase);
+ }
+
+ runNextTest();
+ }
+
+ function step1() {
+ runTests(1, step2);
+ }
+
+ function step2() {
+ toggleShowPasswords(function() {
+ runTests(2, step3);
+ });
+ }
+
+ function step3() {
+ toggleShowPasswords(function() {
+ runTests(3, lastStep);
+ });
+ }
+
+ function lastStep() {
+ // cleanup
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ // unregister ourself
+ Services.ww.unregisterNotification(arguments.callee);
+
+ pwmgr.removeAllLogins();
+ finish();
+ });
+ pwmgrdlg.close();
+ }
+
+ step1();
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js
new file mode 100644
index 000000000..8df89b510
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js
@@ -0,0 +1,144 @@
+/*
+ * Test username selection dialog, on password update from a p-only form,
+ * when there are multiple saved logins on the domain.
+ */
+
+// Copied from prompt_common.js. TODO: share the code.
+function getSelectDialogDoc() {
+ // Trudge through all the open windows, until we find the one
+ // that has selectDialog.xul loaded.
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ // var enumerator = wm.getEnumerator("navigator:browser");
+ var enumerator = wm.getXULWindowEnumerator(null);
+
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+ var containedDocShells = windowDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeChrome,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ // Get the corresponding document for this docshell
+ var childDocShell = containedDocShells.getNext();
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
+ continue;
+ var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+ .contentViewer
+ .DOMDocument;
+
+ if (childDoc.location.href == "chrome://global/content/selectDialog.xul")
+ return childDoc;
+ }
+ }
+
+ return null;
+}
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login1B = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1B", "notifyp1B", "user", "pass");
+
+add_task(function* test_changeUPLoginOnPUpdateForm_accept() {
+ info("Select an u+p login from multiple logins, on password update form, and accept.");
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return getSelectDialogDoc();
+ }, "Wait for selection dialog to be accessible.");
+
+ let doc = getSelectDialogDoc();
+ let dialog = doc.getElementsByTagName("dialog")[0];
+ let listbox = doc.getElementById("list");
+
+ is(listbox.selectedIndex, 0, "Checking selected index");
+ is(listbox.itemCount, 2, "Checking selected length");
+ ['notifyu1', 'notifyu1B'].forEach((username, i) => {
+ is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog");
+ });
+
+ dialog.acceptDialog();
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return !getSelectDialogDoc();
+ }, "Wait for selection dialog to disappear.");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have 2 logins");
+
+ let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(function* test_changeUPLoginOnPUpdateForm_cancel() {
+ info("Select an u+p login from multiple logins, on password update form, and cancel.");
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return getSelectDialogDoc();
+ }, "Wait for selection dialog to be accessible.");
+
+ let doc = getSelectDialogDoc();
+ let dialog = doc.getElementsByTagName("dialog")[0];
+ let listbox = doc.getElementById("list");
+
+ is(listbox.selectedIndex, 0, "Checking selected index");
+ is(listbox.itemCount, 2, "Checking selected length");
+ ['notifyu1', 'notifyu1B'].forEach((username, i) => {
+ is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog");
+ });
+
+ dialog.cancelDialog();
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return !getSelectDialogDoc();
+ }, "Wait for selection dialog to disappear.");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have 2 logins");
+
+ let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ // cleanup
+ Services.logins.removeLogin(login1);
+ Services.logins.removeLogin(login1B);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html
new file mode 100644
index 000000000..76056e375
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head>
+<body onload="document.getElementById('form-basic-username').focus();">
+<!-- Username field is focused by js onload -->
+<form id="form-basic">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_basic.html b/toolkit/components/passwordmgr/test/browser/form_basic.html
new file mode 100644
index 000000000..df2083a93
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html
new file mode 100644
index 000000000..616f56947
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+</head>
+
+<body>
+ <!-- Form in an iframe -->
+ <iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe>
+</body>
+
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html
new file mode 100644
index 000000000..e8aa8b215
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="http://another.domain/custom_action.html">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html
new file mode 100644
index 000000000..892a9f6f6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="https://another.domain/custom_action.html">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html
new file mode 100644
index 000000000..8f0c9a14e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="./custom_action.html">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/formless_basic.html b/toolkit/components/passwordmgr/test/browser/formless_basic.html
new file mode 100644
index 000000000..2f4c5de52
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/formless_basic.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+
+<!-- Simplest form with username and password fields. -->
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+
+ <button id="add">Add input[type=password]</button>
+
+ <script>
+ document.getElementById("add").addEventListener("click", function () {
+ var node = document.createElement("input");
+ node.setAttribute("type", "password");
+ document.querySelector("body").appendChild(node);
+ });
+ </script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/head.js b/toolkit/components/passwordmgr/test/browser/head.js
new file mode 100644
index 000000000..926cb6616
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/head.js
@@ -0,0 +1,137 @@
+const DIRECTORY_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
+
+Cu.import("resource://testing-common/LoginTestUtils.jsm", this);
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+registerCleanupFunction(function* cleanup_removeAllLoginsAndResetRecipes() {
+ Services.logins.removeAllLogins();
+
+ let recipeParent = LoginTestUtils.recipes.getRecipeParent();
+ if (!recipeParent) {
+ // No need to reset the recipes if the recipe module wasn't even loaded.
+ return;
+ }
+ yield recipeParent.then(recipeParentResult => recipeParentResult.reset());
+});
+
+/**
+ * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a
+ * promise resolving with the field values when the optional `aTaskFn` is done.
+ *
+ * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs
+ * @param {Function} aTaskFn - task which can be run before the tab closes.
+ * @param {String} [aOrigin="http://example.com"] - origin of the server to use
+ * to load `aPageFile`.
+ */
+function testSubmittingLoginForm(aPageFile, aTaskFn, aOrigin = "http://example.com") {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: aOrigin + DIRECTORY_PATH + aPageFile,
+ }, function*(browser) {
+ ok(true, "loaded " + aPageFile);
+ let fieldValues = yield ContentTask.spawn(browser, undefined, function*() {
+ yield ContentTaskUtils.waitForCondition(() => {
+ return content.location.pathname.endsWith("/formsubmit.sjs") &&
+ content.document.readyState == "complete";
+ }, "Wait for form submission load (formsubmit.sjs)");
+ let username = content.document.getElementById("user").textContent;
+ let password = content.document.getElementById("pass").textContent;
+ return {
+ username,
+ password,
+ };
+ });
+ ok(true, "form submission loaded");
+ if (aTaskFn) {
+ yield* aTaskFn(fieldValues);
+ }
+ return fieldValues;
+ });
+}
+
+function checkOnlyLoginWasUsedTwice({ justChanged }) {
+ // Check to make sure we updated the timestamps and use count on the
+ // existing login that was submitted for the test.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ ok(logins[0] instanceof Ci.nsILoginMetaInfo, "metainfo QI");
+ is(logins[0].timesUsed, 2, "check .timesUsed for existing login submission");
+ ok(logins[0].timeCreated < logins[0].timeLastUsed, "timeLastUsed bumped");
+ if (justChanged) {
+ is(logins[0].timeLastUsed, logins[0].timePasswordChanged, "timeLastUsed == timePasswordChanged");
+ } else {
+ is(logins[0].timeCreated, logins[0].timePasswordChanged, "timeChanged not updated");
+ }
+}
+
+// Begin popup notification (doorhanger) functions //
+
+const REMEMBER_BUTTON = 0;
+const NEVER_BUTTON = 1;
+
+const CHANGE_BUTTON = 0;
+const DONT_CHANGE_BUTTON = 1;
+
+/**
+ * Checks if we have a password capture popup notification
+ * of the right type and with the right label.
+ *
+ * @param {String} aKind The desired `passwordNotificationType`
+ * @param {Object} [popupNotifications = PopupNotifications]
+ * @return the found password popup notification.
+ */
+function getCaptureDoorhanger(aKind, popupNotifications = PopupNotifications) {
+ ok(true, "Looking for " + aKind + " popup notification");
+ let notification = popupNotifications.getNotification("password");
+ if (notification) {
+ is(notification.options.passwordNotificationType, aKind, "Notification type matches.");
+ if (aKind == "password-change") {
+ is(notification.mainAction.label, "Update", "Main action label matches update doorhanger.");
+ } else if (aKind == "password-save") {
+ is(notification.mainAction.label, "Remember", "Main action label matches save doorhanger.");
+ }
+ }
+ return notification;
+}
+
+/**
+ * Clicks the specified popup notification button.
+ *
+ * @param {Element} aPopup Popup Notification element
+ * @param {Number} aButtonIndex Number indicating which button to click.
+ * See the constants in this file.
+ */
+function clickDoorhangerButton(aPopup, aButtonIndex) {
+ ok(true, "Looking for action at index " + aButtonIndex);
+
+ let notifications = aPopup.owner.panel.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ ok(true, notifications.length + " notification(s)");
+ let notification = notifications[0];
+
+ if (aButtonIndex == 0) {
+ ok(true, "Triggering main action");
+ notification.button.doCommand();
+ } else if (aButtonIndex <= aPopup.secondaryActions.length) {
+ ok(true, "Triggering secondary action " + aButtonIndex);
+ notification.childNodes[aButtonIndex].doCommand();
+ }
+}
+
+/**
+ * Checks the doorhanger's username and password.
+ *
+ * @param {String} username The username.
+ * @param {String} password The password.
+ */
+function* checkDoorhangerUsernamePassword(username, password) {
+ yield BrowserTestUtils.waitForCondition(() => {
+ return document.getElementById("password-notification-username").value == username;
+ }, "Wait for nsLoginManagerPrompter writeDataToUI()");
+ is(document.getElementById("password-notification-username").value, username,
+ "Check doorhanger username");
+ is(document.getElementById("password-notification-password").value, password,
+ "Check doorhanger password");
+}
+
+// End popup notification (doorhanger) functions //
diff --git a/toolkit/components/passwordmgr/test/browser/insecure_test.html b/toolkit/components/passwordmgr/test/browser/insecure_test.html
new file mode 100644
index 000000000..fedea1428
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- This frame is initially loaded over HTTP. -->
+<iframe id="test-iframe"
+ src="http://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html"/>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
new file mode 100644
index 000000000..3f01e36a6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<form>
+ <input name="password" type="password">
+</form>
+
+<!-- Link to reload this page over HTTPS. -->
+<a id="test-link"
+ href="https://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html">HTTPS</a>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/multiple_forms.html b/toolkit/components/passwordmgr/test/browser/multiple_forms.html
new file mode 100644
index 000000000..3f64f8993
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/multiple_forms.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+
+<form class="test-form"
+ description="Password only form">
+ <input id='test-password-1' type='password' name='pname' value=''>
+ <input type='submit'>Submit</input>
+</form>
+
+
+<form class="test-form"
+ description="Username only form">
+ <input id='test-username-1' type='text' name='uname' value=''>
+ <input type='submit'>Submit</input>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form">
+ <input id='test-username-2' type='text' name='uname' value=''>
+ <input id='test-password-2' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password form, prefilled username">
+ <input id='test-username-3' type='text' name='uname' value='testuser'>
+ <input id='test-password-3' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password form, prefilled username and password">
+ <input id='test-username-4' type='text' name='uname' value='testuser'>
+ <input id='test-password-4' type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="One username and two passwords empty form">
+ <input id='test-username-5' type='text' name='uname'>
+ <input id='test-password-5' type='password' name='pname'>
+ <input id='test-password2-5' type='password' name='pname2'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="One username and two passwords form, fields prefiled">
+ <input id='test-username-6' type='text' name='uname' value="testuser">
+ <input id='test-password-6' type='password' name='pname' value="testpass">
+ <input id='test-password2-6' type='password' name='pname2' value="testpass">
+ <button type='submit'>Submit</button>
+</form>
+
+
+<div class="test-form"
+ description="Username and password fields with no form">
+ <input id='test-username-7' type='text' name='uname' value="testuser">
+ <input id='test-password-7' type='password' name='pname' value="testpass">
+</div>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with disabled password">
+ <input id='test-username-8' type='text' name='uname' value=''>
+ <input id='test-password-8' type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with disabled username">
+ <input id='test-username-9' type='text' name='uname' value='' disabled>
+ <input id='test-password-9' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with readonly password">
+ <input id='test-username-10' type='text' name='uname' value=''>
+ <input id='test-password-10' type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with readonly username">
+ <input id='test-username-11' type='text' name='uname' value='' readonly>
+ <input id='test-password-11' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Two username and one passwords form, fields prefiled">
+ <input id='test-username-12' type='text' name='uname' value="testuser">
+ <input id='test-username2-12' type='text' name='uname2' value="testuser">
+ <input id='test-password-12' type='password' name='pname' value="testpass">
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Two username and one passwords form, one disabled username field">
+ <input id='test-username-13' type='text' name='uname'>
+ <input id='test-username2-13' type='text' name='uname2' disabled>
+ <input id='test-password-13' type='password' name='pname'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<div class="test-form"
+ description="Second username and password fields with no form">
+ <input id='test-username-14' type='text' name='uname'>
+ <input id='test-password-14' type='password' name='pname' expectedFail>
+</div>
+
+<!-- Form in an iframe -->
+<iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe>
+
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs
new file mode 100644
index 000000000..84c75437e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "test/content", false);
+}
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html
new file mode 100644
index 000000000..b96faf2ee
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Basic 1un 1pw</title>
+</head>
+<body>
+<h2>Subtest 1</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html
new file mode 100644
index 000000000..2dc96b4fd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 10</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
new file mode 100644
index 000000000..cf3df5275
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Popup Windows</title>
+</head>
+<body>
+<h2>Subtest 11 (popup windows)</h2>
+<script>
+
+// Ignore the '?' and split on |
+[username, password, features, autoClose] = window.location.search.substring(1).split('|');
+
+var url = "subtst_notifications_11_popup.html?" + username + "|" + password;
+var popupWin = window.open(url, "subtst_11", features);
+
+// Popup window will call this function on form submission.
+function formSubmitted() {
+ if (autoClose)
+ popupWin.close();
+}
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
new file mode 100644
index 000000000..2e8e4135c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 11</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ // Get the password from the query string (exclude '?').
+ [username, password] = window.location.search.substring(1).split('|');
+ userField.value = username;
+ passField.value = password;
+ form.submit();
+ window.opener.formSubmitted();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html
new file mode 100644
index 000000000..72651d6c1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - autocomplete=off on the username field</title>
+</head>
+<body>
+<h2>Subtest 2</h2>
+(username autocomplete=off)
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" autocomplete="off">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html
new file mode 100644
index 000000000..7ddbf0851
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications with 2 password fields and no username</title>
+</head>
+<body>
+<h2>Subtest 24</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass1" name="pass1" type="password" value="staticpw">
+ <input id="pass" name="pass" type="password">
+ <button type="submit">Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ pass.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var pass = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html
new file mode 100644
index 000000000..893f18724
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications with 2 password fields and 1 username field and one other text field before the first password field</title>
+</head>
+<body>
+<h2>1 username field followed by a text field followed by 2 username fields</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" value="staticpw">
+ <input id="city" name="city" value="city">
+ <input id="pass" name="pass" type="password">
+ <input id="pin" name="pin" type="password" value="static-pin">
+ <button type="submit">Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html
new file mode 100644
index 000000000..291e735d0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - autocomplete=off on the password field</title>
+</head>
+<body>
+<h2>Subtest 3</h2>
+(password autocomplete=off)
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password" autocomplete="off">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html
new file mode 100644
index 000000000..63df3a42d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" >
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 4</h2>
+(form autocomplete=off)
+<form id="form" action="formsubmit.sjs" autocomplete="off">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html
new file mode 100644
index 000000000..72a3df95f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Form with only a username field</title>
+</head>
+<body>
+<h2>Subtest 5</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html
new file mode 100644
index 000000000..47e23e972
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 6</h2>
+(password-only form)
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html
new file mode 100644
index 000000000..abeea4262
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 8</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "pass2";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html
new file mode 100644
index 000000000..c6f741068
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 9</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "";
+ passField.value = "pass2";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html
new file mode 100644
index 000000000..d74f3bcdf
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Change password</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass_current" name="pass_current" type="password" value="notifyp1">
+ <input id="pass" name="pass" type="password">
+ <input id="pass_confirm" name="pass_confirm" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "pass2";
+ passConfirmField.value = "pass2";
+
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+var passConfirmField = document.getElementById("pass_confirm");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/chrome.ini b/toolkit/components/passwordmgr/test/chrome/chrome.ini
new file mode 100644
index 000000000..093b87b7d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/chrome.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+skip-if = os == 'android'
+
+[test_privbrowsing_perwindowpb.html]
+skip-if = true # Bug 1173337
+support-files =
+ ../formsubmit.sjs
+ notification_common.js
+ privbrowsing_perwindowpb_iframe.html
+ subtst_privbrowsing_1.html
+ subtst_privbrowsing_2.html
+ subtst_privbrowsing_3.html
+ subtst_privbrowsing_4.html
diff --git a/toolkit/components/passwordmgr/test/chrome/notification_common.js b/toolkit/components/passwordmgr/test/chrome/notification_common.js
new file mode 100644
index 000000000..e8a52929d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/notification_common.js
@@ -0,0 +1,111 @@
+/*
+ * Initialization: for each test, remove any prior notifications.
+ */
+function cleanUpPopupNotifications() {
+ var container = getPopupNotifications(window.top);
+ var notes = container._currentNotifications;
+ info(true, "Removing " + notes.length + " popup notifications.");
+ for (var i = notes.length - 1; i >= 0; i--) {
+ notes[i].remove();
+ }
+}
+cleanUpPopupNotifications();
+
+/*
+ * getPopupNotifications
+ *
+ * Fetches the popup notification for the specified window.
+ */
+function getPopupNotifications(aWindow) {
+ var Ci = SpecialPowers.Ci;
+ var Cc = SpecialPowers.Cc;
+ ok(Ci != null, "Access Ci");
+ ok(Cc != null, "Access Cc");
+
+ var chromeWin = SpecialPowers.wrap(aWindow)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.ownerDocument.defaultView;
+
+ var popupNotifications = chromeWin.PopupNotifications;
+ return popupNotifications;
+}
+
+
+/**
+ * Checks if we have a password popup notification
+ * of the right type and with the right label.
+ *
+ * @deprecated Write a browser-chrome test instead and use the fork of this method there.
+ * @returns the found password popup notification.
+ */
+function getPopup(aPopupNote, aKind) {
+ ok(true, "Looking for " + aKind + " popup notification");
+ var notification = aPopupNote.getNotification("password");
+ if (notification) {
+ is(notification.options.passwordNotificationType, aKind, "Notification type matches.");
+ if (aKind == "password-change") {
+ is(notification.mainAction.label, "Update", "Main action label matches update doorhanger.");
+ } else if (aKind == "password-save") {
+ is(notification.mainAction.label, "Remember", "Main action label matches save doorhanger.");
+ }
+ }
+ return notification;
+}
+
+
+/**
+ * @deprecated - Use a browser chrome test instead.
+ *
+ * Clicks the specified popup notification button.
+ */
+function clickPopupButton(aPopup, aButtonIndex) {
+ ok(true, "Looking for action at index " + aButtonIndex);
+
+ var notifications = SpecialPowers.wrap(aPopup.owner).panel.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ ok(true, notifications.length + " notifications");
+ var notification = notifications[0];
+
+ if (aButtonIndex == 0) {
+ ok(true, "Triggering main action");
+ notification.button.doCommand();
+ } else if (aButtonIndex <= aPopup.secondaryActions.length) {
+ var index = aButtonIndex;
+ ok(true, "Triggering secondary action " + index);
+ notification.childNodes[index].doCommand();
+ }
+}
+
+const kRememberButton = 0;
+const kNeverButton = 1;
+
+const kChangeButton = 0;
+const kDontChangeButton = 1;
+
+function dumpNotifications() {
+ try {
+ // PopupNotifications
+ var container = getPopupNotifications(window.top);
+ ok(true, "is popup panel open? " + container.isPanelOpen);
+ var notes = container._currentNotifications;
+ ok(true, "Found " + notes.length + " popup notifications.");
+ for (let i = 0; i < notes.length; i++) {
+ ok(true, "#" + i + ": " + notes[i].id);
+ }
+
+ // Notification bars
+ var chromeWin = SpecialPowers.wrap(window.top)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.ownerDocument.defaultView;
+ var nb = chromeWin.getNotificationBox(window.top);
+ notes = nb.allNotifications;
+ ok(true, "Found " + notes.length + " notification bars.");
+ for (let i = 0; i < notes.length; i++) {
+ ok(true, "#" + i + ": " + notes[i].getAttribute("value"));
+ }
+ } catch (e) { todo(false, "WOAH! " + e); }
+}
diff --git a/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html b/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html
new file mode 100644
index 000000000..2efdab265
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<iframe id="iframe"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html
new file mode 100644
index 000000000..8c7202dd0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+</head>
+<body>
+<h2>Subtest 1</h2>
+<!--
+ Make sure that the password-save notification appears outside of
+ the private mode, but not inside it.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" type="text">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html
new file mode 100644
index 000000000..bf3b85159
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+</head>
+<body>
+<h2>Subtest 2</h2>
+<!--
+ Make sure that the password-change notification appears outside of
+ the private mode, but not inside it.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <input id="newpass" name="newpass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "notifyp1";
+ passField2.value = "notifyp2";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var passField = document.getElementById("pass");
+var passField2 = document.getElementById("newpass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html
new file mode 100644
index 000000000..e88a302e0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+</head>
+<body>
+<h2>Subtest 3</h2>
+<!--
+ Make sure that the user/pass fields are auto-filled outside of
+ the private mode, but not inside it.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" type="text">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ form.submit();
+}
+
+var form = document.getElementById("form");
+window.addEventListener('message', () => { submitForm(); });
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html
new file mode 100644
index 000000000..184142743
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+</head>
+<body>
+<h2>Subtest 4</h2>
+<!--
+ Make sure that the user/pass fields have manual filling enabled
+ in private mode.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" type="text">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function startAutocomplete() {
+ userField.focus();
+ doKey("down");
+ setTimeout(submitForm, 100);
+}
+
+function submitForm() {
+ doKey("down");
+ doKey("return");
+ setTimeout(function() { form.submit(); }, 100);
+}
+
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+
+window.addEventListener('message', () => { startAutocomplete(); });
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html b/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html
new file mode 100644
index 000000000..6b7d4abb3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html
@@ -0,0 +1,322 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=248970
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Private Browsing</title>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="notification_common.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=248970">Mozilla Bug 248970</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 248970 **/
+// based on test_notifications.html
+
+const Ci = SpecialPowers.Ci;
+const Cc = SpecialPowers.Cc;
+const Cr = SpecialPowers.Cr;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var testpath = "/chrome/toolkit/components/passwordmgr/test/chrome/";
+var prefix = "http://test2.example.com" + testpath;
+var subtests = [
+ "subtst_privbrowsing_1.html", // 1
+ "subtst_privbrowsing_1.html", // 2
+ "subtst_privbrowsing_1.html", // 3
+ "subtst_privbrowsing_2.html", // 4
+ "subtst_privbrowsing_2.html", // 5
+ "subtst_privbrowsing_2.html", // 6
+ "subtst_privbrowsing_3.html", // 7
+ "subtst_privbrowsing_3.html", // 8
+ "subtst_privbrowsing_4.html", // 9
+ "subtst_privbrowsing_3.html" // 10
+ ];
+var observer;
+
+var testNum = 0;
+function loadNextTest() {
+ // run the initialization code for each test
+ switch (++ testNum) {
+ case 1:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ case 2:
+ popupNotifications = privateWindowPopupNotifications;
+ iframe = privateWindowIframe;
+ break;
+
+ case 3:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ case 4:
+ pwmgr.addLogin(login);
+ break;
+
+ case 5:
+ popupNotifications = privateWindowPopupNotifications;
+ iframe = privateWindowIframe;
+ break;
+
+ case 6:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ case 7:
+ pwmgr.addLogin(login);
+ break;
+
+ case 8:
+ popupNotifications = privateWindowPopupNotifications;
+ iframe = privateWindowIframe;
+ break;
+
+ case 9:
+ break;
+
+ case 10:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ default:
+ ok(false, "Unexpected call to loadNextTest for test #" + testNum);
+ }
+
+ if (testNum === 7) {
+ observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
+ SimpleTest.executeSoon(() => { iframe.contentWindow.postMessage("go", "*"); });
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
+ }
+
+ ok(true, "Starting test #" + testNum);
+ iframe.src = prefix + subtests[testNum - 1];
+}
+
+function checkTest() {
+ var popup;
+ var gotUser;
+ var gotPass;
+
+ switch (testNum) {
+ case 1:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-save");
+ ok(popup, "got popup notification");
+ popup.remove();
+ break;
+
+ case 2:
+ // run inside of private mode, popup notification should not appear
+ popup = getPopup(popupNotifications, "password-save");
+ ok(!popup, "checking for no popup notification");
+ break;
+
+ case 3:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-save");
+ ok(popup, "got popup notification");
+ popup.remove();
+ break;
+
+ case 4:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-change");
+ ok(popup, "got popup notification");
+ popup.remove();
+ break;
+
+ case 5:
+ // run inside of private mode, popup notification should not appear
+ popup = getPopup(popupNotifications, "password-change");
+ ok(!popup, "checking for no popup notification");
+ break;
+
+ case 6:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-change");
+ ok(popup, "got popup notification");
+ popup.remove();
+ pwmgr.removeLogin(login);
+ break;
+
+ case 7:
+ // verify that the user/pass pair was autofilled
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "notifyu1", "Checking submitted username");
+ is(gotPass, "notifyp1", "Checking submitted password");
+ break;
+
+ case 8:
+ // verify that the user/pass pair was not autofilled
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "", "Checking submitted username");
+ is(gotPass, "", "Checking submitted password");
+ break;
+
+ case 9:
+ // verify that the user/pass pair was available for autocomplete
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "notifyu1", "Checking submitted username");
+ is(gotPass, "notifyp1", "Checking submitted password");
+ break;
+
+ case 10:
+ // verify that the user/pass pair was autofilled
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "notifyu1", "Checking submitted username");
+ is(gotPass, "notifyp1", "Checking submitted password");
+ pwmgr.removeLogin(login);
+ break;
+
+ default:
+ ok(false, "Unexpected call to checkTest for test #" + testNum);
+
+ }
+}
+
+var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+var contentPage = "http://mochi.test:8888/chrome/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html";
+var testWindows = [];
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function obs(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(obs, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function testOnWindow(aIsPrivate, aCallback) {
+ var win = mainWindow.OpenBrowserWindow({private: aIsPrivate});
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ whenDelayedStartupFinished(win, function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ win.gBrowser.loadURI(contentPage);
+ return;
+ }
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+
+ win.content.addEventListener('load', function innerLoad2() {
+ win.content.removeEventListener('load', innerLoad2, false);
+ testWindows.push(win);
+ SimpleTest.executeSoon(function() { aCallback(win); });
+ }, false, true);
+ }, true);
+ SimpleTest.executeSoon(function() { win.gBrowser.loadURI(contentPage); });
+ });
+ }, true);
+}
+
+var ignoreLoad = false;
+function handleLoad(aEvent) {
+ // ignore every other load event ... We get one for loading the subtest (which
+ // we want to ignore), and another when the subtest's form submits itself
+ // (which we want to handle, to start the next test).
+ ignoreLoad = !ignoreLoad;
+ if (ignoreLoad) {
+ ok(true, "Ignoring load of subtest #" + testNum);
+ return;
+ }
+ ok(true, "Processing submission of subtest #" + testNum);
+
+ checkTest();
+
+ if (testNum < subtests.length) {
+ loadNextTest();
+ } else {
+ ok(true, "private browsing notification tests finished.");
+
+ testWindows.forEach(function(aWin) {
+ aWin.close();
+ });
+
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ SimpleTest.finish();
+ }
+}
+
+var pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ok(pwmgr != null, "Access pwmgr");
+
+// We need to make sure no logins have been stored by previous tests
+// for forms in |url|, otherwise the change password notification
+// would turn into a prompt, and the test will fail.
+var url = "http://test2.example.com";
+is(pwmgr.countLogins(url, "", null), 0, "No logins should be stored for " + url);
+
+var nsLoginInfo = new SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+var login = new nsLoginInfo(url, url, null, "notifyu1", "notifyp1", "user", "pass");
+
+var normalWindow;
+var privateWindow;
+
+var iframe;
+var normalWindowIframe;
+var privateWindowIframe;
+
+var popupNotifications;
+var normalWindowPopupNotifications;
+var privateWindowPopupNotifications;
+
+testOnWindow(false, function(aWin) {
+ var selectedBrowser = aWin.gBrowser.selectedBrowser;
+ normalWindowIframe = selectedBrowser.contentDocument.getElementById("iframe");
+ normalWindowIframe.onload = handleLoad;
+ selectedBrowser.focus();
+
+ normalWindowPopupNotifications = getPopupNotifications(selectedBrowser.contentWindow.top);
+ ok(normalWindowPopupNotifications, "Got popupNotifications in normal window");
+ // ignore the first load for this window;
+ ignoreLoad = false;
+
+ testOnWindow(true, function(aPrivateWin) {
+ selectedBrowser = aPrivateWin.gBrowser.selectedBrowser;
+ privateWindowIframe = selectedBrowser.contentDocument.getElementById("iframe");
+ privateWindowIframe.onload = handleLoad;
+ selectedBrowser.focus();
+
+ privateWindowPopupNotifications = getPopupNotifications(selectedBrowser.contentWindow.top);
+ ok(privateWindowPopupNotifications, "Got popupNotifications in private window");
+ // ignore the first load for this window;
+ ignoreLoad = false;
+
+ SimpleTest.executeSoon(loadNextTest);
+ });
+});
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/chrome_timeout.js b/toolkit/components/passwordmgr/test/chrome_timeout.js
new file mode 100644
index 000000000..9049d0bea
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome_timeout.js
@@ -0,0 +1,11 @@
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+addMessageListener('setTimeout', msg => {
+ let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
+ timer.init(_ => {
+ sendAsyncMessage('timeout');
+ }, msg.delay, Ci.nsITimer.TYPE_ONE_SHOT);
+});
+
+sendAsyncMessage('ready');
diff --git a/toolkit/components/passwordmgr/test/formsubmit.sjs b/toolkit/components/passwordmgr/test/formsubmit.sjs
new file mode 100644
index 000000000..4b4a387f7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/formsubmit.sjs
@@ -0,0 +1,37 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true;
+
+ // XXX I bet this doesn't work for POST requests.
+ var query = request.queryString;
+
+ var user = null, pass = null;
+ // user=xxx
+ match = /user=([^&]*)/.exec(query);
+ if (match)
+ user = match[1];
+
+ // pass=xxx
+ match = /pass=([^&]*)/.exec(query);
+ if (match)
+ pass = match[1];
+
+ response.setStatusLine("1.0", 200, "OK");
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>User: <span id='user'>" + user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + pass + "</span></p>\n");
+ response.write("</html>");
+}
diff --git a/toolkit/components/passwordmgr/test/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest.ini
new file mode 100644
index 000000000..640f5c256
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+skip-if = e10s
+support-files =
+ authenticate.sjs
+ blank.html
+ formsubmit.sjs
+ prompt_common.js
+ pwmgr_common.js
+ subtst_master_pass.html
+ subtst_prompt_async.html
+ chrome_timeout.js
+
+[test_master_password.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_prompt_async.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_xhr.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_xml_load.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
diff --git a/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs
new file mode 100644
index 000000000..d2f650013
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs
@@ -0,0 +1,220 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true, requestProxyAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "", expected_pass = "", realm = "mochitest";
+ var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+ var huge = false, plugin = false, anonymous = false;
+ var authHeaderCount = 1;
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match)
+ expected_user = match[1];
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match)
+ expected_pass = match[1];
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match)
+ realm = match[1];
+
+ // proxy_user=xxx
+ match = /proxy_user=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_user = match[1];
+
+ // proxy_pass=xxx
+ match = /proxy_pass=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_pass = match[1];
+
+ // proxy_realm=xxx
+ match = /proxy_realm=([^&]*)/.exec(query);
+ if (match)
+ proxy_realm = match[1];
+
+ // huge=1
+ match = /huge=1/.exec(query);
+ if (match)
+ huge = true;
+
+ // plugin=1
+ match = /plugin=1/.exec(query);
+ if (match)
+ plugin = true;
+
+ // multiple=1
+ match = /multiple=([^&]*)/.exec(query);
+ if (match)
+ authHeaderCount = match[1]+0;
+
+ // anonymous=1
+ match = /anonymous=1/.exec(query);
+ if (match)
+ anonymous = true;
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ var proxy_actual_user = "", proxy_actual_pass = "";
+ if (request.hasHeader("Proxy-Authorization")) {
+ authHeader = request.getHeader("Proxy-Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ proxy_actual_user = match[1];
+ proxy_actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+ if (proxy_expected_user == proxy_actual_user &&
+ proxy_expected_pass == proxy_actual_pass) {
+ requestProxyAuth = false;
+ }
+
+ if (anonymous) {
+ if (authPresent) {
+ response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+ } else {
+ response.setStatusLine("1.0", 200, "Authorization header not found");
+ }
+ } else {
+ if (requestProxyAuth) {
+ response.setStatusLine("1.0", 407, "Proxy authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+ } else if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+ }
+
+ if (plugin) {
+ response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n");
+ }
+
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
new file mode 100644
index 000000000..a4170d7e0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -0,0 +1,69 @@
+[DEFAULT]
+support-files =
+ ../../../prompts/test/chromeScript.js
+ ../../../prompts/test/prompt_common.js
+ ../../../satchel/test/parent_utils.js
+ ../../../satchel/test/satchel_common.js
+ ../authenticate.sjs
+ ../blank.html
+ ../browser/form_autofocus_js.html
+ ../browser/form_basic.html
+ ../browser/form_cross_origin_secure_action.html
+ ../pwmgr_common.js
+ auth2/authenticate.sjs
+
+[test_autocomplete_https_upgrade.html]
+skip-if = toolkit == 'android' # autocomplete
+[test_autofill_https_upgrade.html]
+skip-if = toolkit == 'android' # Bug 1259768
+[test_autofill_password-only.html]
+[test_autofocus_js.html]
+skip-if = toolkit == 'android' # autocomplete
+[test_basic_form.html]
+[test_basic_form_0pw.html]
+[test_basic_form_1pw.html]
+[test_basic_form_1pw_2.html]
+[test_basic_form_2pw_1.html]
+[test_basic_form_2pw_2.html]
+[test_basic_form_3pw_1.html]
+[test_basic_form_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_insecure_form_field_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_password_field_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_insecure_form_field_no_saved_login.html]
+skip-if = toolkit == 'android' || os == 'linux' # android:autocomplete., linux: bug 1325778
+[test_basic_form_html5.html]
+[test_basic_form_pwevent.html]
+[test_basic_form_pwonly.html]
+[test_bug_627616.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_bug_776171.html]
+[test_case_differences.html]
+skip-if = toolkit == 'android' # autocomplete
+[test_form_action_1.html]
+[test_form_action_2.html]
+[test_form_action_javascript.html]
+[test_formless_autofill.html]
+[test_formless_submit.html]
+[test_formless_submit_navigation.html]
+[test_formless_submit_navigation_negative.html]
+[test_input_events.html]
+[test_input_events_for_identical_values.html]
+[test_maxlength.html]
+[test_passwords_in_type_password.html]
+[test_prompt.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_http.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_noWindow.html]
+skip-if = e10s || toolkit == 'android' # Tests desktop prompts. e10s: bug 1217876
+[test_prompt_promptAuth.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_promptAuth_proxy.html]
+skip-if = e10s || os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_recipe_login_fields.html]
+[test_username_focus.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_xhr_2.html]
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
new file mode 100644
index 000000000..7d5725322
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
@@ -0,0 +1,218 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const chromeScript = runChecksAfterCommonInit(false);
+
+runInParent(function addLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ let nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+ // We have two actual HTTPS to avoid autofill before the schemeUpgrades pref flips to true.
+ let login0 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name", "pass", "uname", "pword");
+
+ let login1 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name1", "pass1", "uname", "pword");
+
+ // Same as above but HTTP instead of HTTPS (to test de-duping)
+ let login2 = new nsLoginInfo("http://example.org", "http://example.org", null,
+ "name1", "passHTTP", "uname", "pword");
+
+ // Different HTTP login to upgrade with secure formSubmitURL
+ let login3 = new nsLoginInfo("http://example.org", "https://example.org", null,
+ "name2", "passHTTPtoHTTPS", "uname", "pword");
+
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe src="https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
+let iframeDoc;
+let uname;
+let pword;
+
+// Restore the form to the default state.
+function restoreForm() {
+ pword.focus();
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ let formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username");
+ is(pword.value, expectedPassword, "Checking " + formID + " password");
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
+
+ yield new Promise(resolve => {
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ resolve();
+ });
+ });
+
+ iframeDoc = iframe.contentDocument;
+ uname = iframeDoc.getElementById("form-basic-username");
+ pword = iframeDoc.getElementById("form-basic-password");
+});
+
+add_task(function* test_empty_first_entry() {
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ // Trigger autocomplete popup
+ restoreForm();
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+ let shownPromise = promiseACShown();
+ doKey("down");
+ let results = yield shownPromise;
+ popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected");
+ checkArrayValues(results, ["name", "name1", "name2"], "initial");
+
+ // Check first entry
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name", "pass");
+});
+
+add_task(function* test_empty_second_entry() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name1", "pass1");
+});
+
+add_task(function* test_search() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ // We need to blur for the autocomplete controller to notice the forced value below.
+ uname.blur();
+ uname.value = "name";
+ uname.focus();
+ sendChar("1");
+ doKey("down"); // open
+ let results = yield shownPromise;
+ checkArrayValues(results, ["name1"], "check result deduping for 'name1'");
+ doKey("down"); // first
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name1", "pass1");
+
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is now closed");
+});
+
+add_task(function* test_delete_first_entry() {
+ restoreForm();
+ uname.focus();
+ let shownPromise = promiseACShown();
+ doKey("down");
+ yield shownPromise;
+
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+
+ let deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+ checkACForm("", "");
+
+ let results = yield notifyMenuChanged(2, "name1");
+
+ checkArrayValues(results, ["name1", "name2"], "two should remain after deleting the first");
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup stays open after deleting");
+ doKey("escape");
+ popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup closed upon ESC");
+});
+
+add_task(function* test_delete_duplicate_entry() {
+ restoreForm();
+ uname.focus();
+ let shownPromise = promiseACShown();
+ doKey("down");
+ yield shownPromise;
+
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+
+ let deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+ checkACForm("", "");
+
+ is(LoginManager.countLogins("http://example.org", "http://example.org", null), 1,
+ "Check that the HTTP login remains");
+ is(LoginManager.countLogins("https://example.org", "https://example.org", null), 0,
+ "Check that the HTTPS login was deleted");
+
+ // Two menu items should remain as the HTTPS login should have been deleted but
+ // the HTTP would remain.
+ let results = yield notifyMenuChanged(1, "name2");
+
+ checkArrayValues(results, ["name2"], "one should remain after deleting the HTTPS name1");
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup stays open after deleting");
+ doKey("escape");
+ popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup closed upon ESC");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
new file mode 100644
index 000000000..ee1424002
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
+const CROSS_ORIGIN_SECURE_PATH = TESTS_DIR + "mochitest/form_cross_origin_secure_action.html";
+
+const chromeScript = runChecksAfterCommonInit(false);
+
+let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+SpecialPowers.Ci.nsILoginInfo,
+"init");
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ let iframeDoc = iframe.contentDocument;
+ let uname = iframeDoc.getElementById("form-basic-username");
+ let pword = iframeDoc.getElementById("form-basic-password");
+ let formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username");
+ is(pword.value, expectedPassword, "Checking " + formID + " password");
+}
+function* prepareLoginsAndProcessForm(url, logins = []) {
+ LoginManager.removeAllLogins();
+
+ let dates = Date.now();
+ for (let login of logins) {
+ SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo);
+ // Force all dates to be the same so they don't affect things like deduping.
+ login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates;
+ LoginManager.addLogin(login);
+ }
+
+ iframe.src = url;
+ yield promiseFormsProcessed();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
+});
+
+add_task(function* test_simpleNoDupesNoAction() {
+ yield prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
+ new nsLoginInfo("http://example.com", "http://example.com", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_simpleNoDupesUpgradeOriginAndAction() {
+ yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("http://example.com", "http://another.domain", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_simpleNoDupesUpgradeOriginOnly() {
+ yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("http://example.com", "https://another.domain", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_simpleNoDupesUpgradeActionOnly() {
+ yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("https://example.com", "http://another.domain", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_dedupe() {
+ yield prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
+ new nsLoginInfo("https://example.com", "https://example.com", null,
+ "name1", "passHTTPStoHTTPS", "uname", "pword"),
+ new nsLoginInfo("http://example.com", "http://example.com", null,
+ "name1", "passHTTPtoHTTP", "uname", "pword"),
+ new nsLoginInfo("http://example.com", "https://example.com", null,
+ "name1", "passHTTPtoHTTPS", "uname", "pword"),
+ new nsLoginInfo("https://example.com", "http://example.com", null,
+ "name1", "passHTTPStoHTTP", "uname", "pword"),
+ ]);
+
+ checkACForm("name1", "passHTTPStoHTTPS");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
new file mode 100644
index 000000000..983356371
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test password-only forms should prefer a password-only login when present</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 444968
+<script>
+let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+pwmgrCommonScript.sendSyncMessage("setupParent", { selfFilling: true });
+
+SimpleTest.waitForExplicitFinish();
+
+let chromeScript = runInParent(function chromeSetup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+
+ let login1A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login1B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1A.init("http://mochi.test:8888", "http://bug444968-1", null,
+ "testuser1A", "testpass1A", "", "");
+ login1B.init("http://mochi.test:8888", "http://bug444968-1", null,
+ "", "testpass1B", "", "");
+
+ login2A.init("http://mochi.test:8888", "http://bug444968-2", null,
+ "testuser2A", "testpass2A", "", "");
+ login2B.init("http://mochi.test:8888", "http://bug444968-2", null,
+ "", "testpass2B", "", "");
+ login2C.init("http://mochi.test:8888", "http://bug444968-2", null,
+ "testuser2C", "testpass2C", "", "");
+
+ pwmgr.addLogin(login1A);
+ pwmgr.addLogin(login1B);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+ pwmgr.addLogin(login2C);
+
+ addMessageListener("removeLogins", function removeLogins() {
+ pwmgr.removeLogin(login1A);
+ pwmgr.removeLogin(login1B);
+ pwmgr.removeLogin(login2A);
+ pwmgr.removeLogin(login2B);
+ pwmgr.removeLogin(login2C);
+ });
+});
+
+SimpleTest.registerCleanupFunction(() => chromeScript.sendSyncMessage("removeLogins"));
+
+registerRunTests();
+</script>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- first 3 forms have matching user+pass and pass-only logins -->
+
+ <!-- user+pass form. -->
+ <form id="form1" action="http://bug444968-1">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password-only form. -->
+ <form id="form2" action="http://bug444968-1">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form3" action="http://bug444968-1">
+ <input type="text" name="uname" value="testuser1A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+
+ <!-- next 4 forms have matching user+pass (2x) and pass-only (1x) logins -->
+
+ <!-- user+pass form. -->
+ <form id="form4" action="http://bug444968-2">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password-only form. -->
+ <form id="form5" action="http://bug444968-2">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form6" action="http://bug444968-2">
+ <input type="text" name="uname" value="testuser2A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form7" action="http://bug444968-2">
+ <input type="text" name="uname" value="testuser2C">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 444968 (password-only forms should prefer a
+ * password-only login when present )
+ */
+function startTest() {
+ checkForm(1, "testuser1A", "testpass1A");
+ checkForm(2, "testpass1B");
+ checkForm(3, "testuser1A", "testpass1A");
+
+ checkUnmodifiedForm(4); // 2 logins match
+ checkForm(5, "testpass2B");
+ checkForm(6, "testuser2A", "testpass2A");
+ checkForm(7, "testuser2C", "testpass2C");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("runTests", startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
new file mode 100644
index 000000000..2ce3293dd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test login autocomplete is activated when focused by js on load</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const chromeScript = runChecksAfterCommonInit(false);
+
+runInParent(function addLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ let nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+ let login0 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name", "pass", "uname", "pword");
+
+ let login1 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name1", "pass1", "uname", "pword");
+
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+});
+</script>
+<p id="display"></p>
+
+<div id="content">
+ <iframe src="https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/form_autofocus_js.html"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
+let iframeDoc;
+
+add_task(function* setup() {
+ yield new Promise(resolve => {
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ resolve();
+ });
+ });
+
+ iframeDoc = iframe.contentDocument;
+
+ SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur");
+});
+
+add_task(function* test_initial_focus() {
+ let results = yield notifyMenuChanged(2, "name");
+ checkArrayValues(results, ["name", "name1"], "Two results");
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ is(iframeDoc.getElementById("form-basic-password").value, "pass", "Check first password filled");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is now closed");
+});
+
+// This depends on the filling from the previous test.
+add_task(function* test_not_reopened_if_filled() {
+ listenForUnexpectedPopupShown();
+ let usernameField = iframeDoc.getElementById("form-basic-username");
+ usernameField.focus();
+ info("Waiting to see if a popupshown occurs");
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+
+ // cleanup
+ gPopupShownExpected = true;
+ iframeDoc.getElementById("form-basic-submit").focus();
+});
+
+add_task(function* test_reopened_after_edit_not_matching_saved() {
+ let usernameField = iframeDoc.getElementById("form-basic-username");
+ usernameField.value = "nam";
+ let shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+ iframeDoc.getElementById("form-basic-submit").focus();
+});
+
+add_task(function* test_not_reopened_after_selecting() {
+ let formFillController = SpecialPowers.Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(SpecialPowers.Ci.nsIFormFillController);
+ let usernameField = iframeDoc.getElementById("form-basic-username");
+ usernameField.value = "";
+ iframeDoc.getElementById("form-basic-password").value = "";
+ listenForUnexpectedPopupShown();
+ formFillController.markAsLoginManagerField(usernameField);
+ info("Waiting to see if a popupshown occurs");
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Cleanup
+ gPopupShownExpected = true;
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html
new file mode 100644
index 000000000..3c38343a5
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: simple form fill
+
+<script>
+runChecksAfterCommonInit(startTest);
+
+/** Test for Login Manager: form fill, multiple forms. **/
+
+function startTest() {
+ is($_(1, "uname").value, "testuser", "Checking for filled username");
+ is($_(1, "pword").value, "testpass", "Checking for filled password");
+
+ SimpleTest.finish();
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
new file mode 100644
index 000000000..0b416673b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with no password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with no password fields
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <!-- Form with no user field or password field -->
+ <form id="form1" action="formtest.js">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with no user field or password field, but one other field -->
+ <form id="form2" action="formtest.js">
+ <input type="checkbox">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with no user field or password field, but one other field -->
+ <form id="form3" action="formtest.js">
+ <input type="checkbox" name="uname" value="">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with a text field, but no password field -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="yyyyy">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with a user field, but no password field -->
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, no password fields. **/
+
+function startTest() {
+ is($_(3, "uname").value, "", "Checking for unfilled checkbox (form 3)");
+ is($_(4, "yyyyy").value, "", "Checking for unfilled text field (form 4)");
+ is($_(5, "uname").value, "", "Checking for unfilled text field (form 5)");
+
+ SimpleTest.finish();
+}
+
+runChecksAfterCommonInit(startTest);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html
new file mode 100644
index 000000000..3937fad4b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 1 password field</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 1 password field
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+<!-- no username fields -->
+
+<form id='form1' action='formtest.js'> 1
+ <!-- Blank, so fill in the password -->
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form2' action='formtest.js'> 2
+ <!-- Already contains the password, so nothing to do. -->
+ <input type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form3' action='formtest.js'> 3
+ <!-- Contains unknown password, so don't change it -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<!-- username fields -->
+
+<form id='form4' action='formtest.js'> 4
+ <!-- Blanks, so fill in login -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form5' action='formtest.js'> 5
+ <!-- Username already set, so fill in password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form6' action='formtest.js'> 6
+ <!-- Unknown username, so don't fill in password -->
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form7' action='formtest.js'> 7
+ <!-- Password already set, could fill in username but that's weird so we don't -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form8' action='formtest.js'> 8
+ <!-- Unknown password, so don't fill in a username -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+
+<!-- extra text fields -->
+
+<form id='form9' action='formtest.js'> 9
+ <!-- text field _after_ password should never be treated as a username field -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form10' action='formtest.js'> 10
+ <!-- only the first text field before the password should be for username -->
+ <input type='text' name='other' value=''>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form11' action='formtest.js'> 11
+ <!-- variation just to make sure extra text field is still ignored -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='text' name='other' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+
+<!-- same as last bunch, but with xxxx in the extra field. -->
+
+<form id='form12' action='formtest.js'> 12
+ <!-- text field _after_ password should never be treated as a username field -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form13' action='formtest.js'> 13
+ <!-- only the first text field before the password should be for username -->
+ <input type='text' name='other' value='xxxxxxxx'>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form14' action='formtest.js'> 14
+ <!-- variation just to make sure extra text field is still ignored -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='text' name='other' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill **/
+
+function startTest() {
+ var f = 1;
+
+ // 1-3
+ checkForm(f++, "testpass");
+ checkForm(f++, "testpass");
+ checkForm(f++, "xxxxxxxx");
+
+ // 4-8
+ checkForm(f++, "testuser", "testpass");
+ checkForm(f++, "testuser", "testpass");
+ checkForm(f++, "xxxxxxxx", "");
+ checkForm(f++, "", "testpass");
+ checkForm(f++, "", "xxxxxxxx");
+
+ // 9-14
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "", "testuser", "testpass");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "testpass", "xxxxxxxx");
+ checkForm(f++, "xxxxxxxx", "testuser", "testpass");
+ checkForm(f++, "testuser", "testpass", "xxxxxxxx");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html
new file mode 100644
index 000000000..0f6566b9c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with 1 password field, part 2</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 1 password field, part 2
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+<form id='form1' action='formtest.js'> 1
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form2' action='formtest.js'> 2
+ <input type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form3' action='formtest.js'> 3
+ <input type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form4' action='formtest.js'> 4
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form5' action='formtest.js'> 5
+ <input type='text' name='uname' value='' disabled>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form6' action='formtest.js'> 6
+ <input type='text' name='uname' value='' readonly>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form7' action='formtest.js'> 7
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form8' action='formtest.js'> 8
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form9' action='formtest.js'> 9
+ <input type='text' name='uname' value='TESTUSER'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form10' action='formtest.js'> 10
+ <input type='text' name='uname' value='TESTUSER' readonly>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form11' action='formtest.js'> 11
+ <input type='text' name='uname' value='TESTUSER' disabled>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill, part 2 **/
+
+function startTest() {
+ var f;
+
+ // Test various combinations of disabled/readonly inputs
+ checkForm(1, "testpass"); // control
+ checkUnmodifiedForm(2);
+ checkUnmodifiedForm(3);
+ checkForm(4, "testuser", "testpass"); // control
+ for (f = 5; f <= 8; f++) { checkUnmodifiedForm(f); }
+ // Test case-insensitive comparison of username field
+ checkForm(9, "testuser", "testpass");
+ checkForm(10, "TESTUSER", "testpass");
+ checkForm(11, "TESTUSER", "testpass");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html
new file mode 100644
index 000000000..128ffca7c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 2 password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 2 password fields
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+
+<!-- no username fields -->
+
+<form id='form1' action='formtest.js'> 1
+ <!-- simple form, fill in first pw -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form2' action='formtest.js'> 2
+ <!-- same but reverse pname and qname, field names are ignored. -->
+ <input type='password' name='qname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form3' action='formtest.js'> 3
+ <!-- text field after password fields should be ignored, no username. -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <input type='text' name='uname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form4' action='formtest.js'> 4
+ <!-- nothing to do, password already present -->
+ <input type='password' name='pname' value='testpass'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form5' action='formtest.js'> 5
+ <!-- don't clobber an existing unrecognized password -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form6' action='formtest.js'> 6
+ <!-- fill in first field, 2nd field shouldn't be touched anyway. -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+
+<!-- with username fields -->
+
+
+
+<form id='form7' action='formtest.js'> 7
+ <!-- simple form, should fill in username and first pw -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form8' action='formtest.js'> 8
+ <!-- reverse pname and qname, field names are ignored. -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form9' action='formtest.js'> 9
+ <!-- username already filled, so just fill first password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form10' action='formtest.js'> 10
+ <!-- unknown username, don't fill in a password -->
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form11' action='formtest.js'> 11
+ <!-- don't clobber unknown password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form12' action='formtest.js'> 12
+ <!-- fill in 1st pass, don't clobber 2nd pass -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form13' action='formtest.js'> 13
+ <!-- nothing to do, user and pass prefilled. life is easy. -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value='testpass'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form14' action='formtest.js'> 14
+ <!-- shouldn't fill in username because 1st pw field is unknown. -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form15' action='formtest.js'> 15
+ <!-- textfield in the middle of pw fields should be ignored -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form16' action='formtest.js'> 16
+ <!-- same, and don't clobber existing unknown password -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill **/
+
+function startTest() {
+ var f = 1;
+
+ // 1-6 no username
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "testpass", "", "");
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "xxxxxxxx", "");
+ checkForm(f++, "testpass", "xxxxxxxx");
+
+ // 7-15 with username
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "xxxxxxxx", "", "");
+ checkForm(f++, "testuser", "xxxxxxxx", "");
+ checkForm(f++, "testuser", "testpass", "xxxxxxxx");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "", "xxxxxxxx", "testpass");
+ checkForm(f++, "testpass", "", "");
+ checkForm(f++, "xxxxxxxx", "", "");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
new file mode 100644
index 000000000..eba811cf9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for form fill with 2 password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: form fill, 2 password fields
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, 2 password fields **/
+
+/*
+ * If a form has two password fields, other things may be going on....
+ *
+ * 1 - The user might be creating a new login (2nd field for typo checking)
+ * 2 - The user is changing a password (old and new password each have field)
+ *
+ * This test is for case #1.
+ */
+
+var numSubmittedForms = 0;
+var numStartingLogins = 0;
+
+function startTest() {
+ // Check for unfilled forms
+ is($_(1, "uname").value, "", "Checking username 1");
+ is($_(1, "pword").value, "", "Checking password 1A");
+ is($_(1, "qword").value, "", "Checking password 1B");
+
+ // Fill in the username and password fields, for account creation.
+ // Form 1
+ $_(1, "uname").value = "newuser1";
+ $_(1, "pword").value = "newpass1";
+ $_(1, "qword").value = "newpass1";
+
+ var button = getFormSubmitButton(1);
+
+ todo(false, "form submission disabled, can't auto-accept dialog yet");
+ SimpleTest.finish();
+}
+
+
+// Called by each form's onsubmit handler.
+function checkSubmit(formNum) {
+ numSubmittedForms++;
+
+ // End the test at the last form.
+ if (formNum == 999) {
+ is(numSubmittedForms, 999, "Ensuring all forms submitted for testing.");
+
+ var numEndingLogins = LoginManager.countLogins("", "", "");
+
+ ok(numEndingLogins > 0, "counting logins at end");
+ is(numStartingLogins, numEndingLogins + 222, "counting logins at end");
+
+ SimpleTest.finish();
+ return false; // return false to cancel current form submission
+ }
+
+ // submit the next form.
+ var button = getFormSubmitButton(formNum + 1);
+ button.click();
+
+ return false; // return false to cancel current form submission
+}
+
+
+function getFormSubmitButton(formNum) {
+ var form = $("form" + formNum); // by id, not name
+ ok(form != null, "getting form " + formNum);
+
+ // we can't just call form.submit(), because that doesn't seem to
+ // invoke the form onsubmit handler.
+ var button = form.firstChild;
+ while (button && button.type != "submit") { button = button.nextSibling; }
+ ok(button != null, "getting form submit button");
+
+ return button;
+}
+
+runChecksAfterCommonInit(startTest);
+
+</script>
+</pre>
+<div id="content" style="display: none">
+ <form id="form1" onsubmit="return checkSubmit(1)" action="http://newuser.com">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html
new file mode 100644
index 000000000..30b5a319f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 3 password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 3 password fields (form filling)
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <p>The next three forms are <b>user/pass/passB/passC</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form2" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword" value="testpass">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+
+ <p>The next three forms are <b>user/passB/pass/passC</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="qword">
+ <input type="password" name="pword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="pword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="pword" value="testpass">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <p>The next three forms are <b>user/passB/passC/pass</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form8" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form9" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword" value="testpass">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, 3 password fields **/
+
+// Test to make sure 3-password forms are filled properly.
+
+function startTest() {
+ // Check form 1
+ is($_(1, "uname").value, "testuser", "Checking username 1");
+ is($_(1, "pword").value, "testpass", "Checking password 1");
+ is($_(1, "qword").value, "", "Checking password 1 (q)");
+ is($_(1, "rword").value, "", "Checking password 1 (r)");
+ // Check form 2
+ is($_(2, "uname").value, "testuser", "Checking username 2");
+ is($_(2, "pword").value, "testpass", "Checking password 2");
+ is($_(2, "qword").value, "", "Checking password 2 (q)");
+ is($_(2, "rword").value, "", "Checking password 2 (r)");
+ // Check form 3
+ is($_(3, "uname").value, "testuser", "Checking username 3");
+ is($_(3, "pword").value, "testpass", "Checking password 3");
+ is($_(3, "qword").value, "", "Checking password 3 (q)");
+ is($_(3, "rword").value, "", "Checking password 3 (r)");
+
+ // Check form 4
+ is($_(4, "uname").value, "testuser", "Checking username 4");
+ todo_is($_(4, "qword").value, "", "Checking password 4 (q)");
+ todo_is($_(4, "pword").value, "testpass", "Checking password 4");
+ is($_(4, "rword").value, "", "Checking password 4 (r)");
+ // Check form 5
+ is($_(5, "uname").value, "testuser", "Checking username 5");
+ todo_is($_(5, "qword").value, "", "Checking password 5 (q)");
+ todo_is($_(5, "pword").value, "testpass", "Checking password 5");
+ is($_(5, "rword").value, "", "Checking password 5 (r)");
+ // Check form 6
+ is($_(6, "uname").value, "testuser", "Checking username 6");
+ todo_is($_(6, "qword").value, "", "Checking password 6 (q)");
+ is($_(6, "pword").value, "testpass", "Checking password 6");
+ is($_(6, "rword").value, "", "Checking password 6 (r)");
+
+ // Check form 7
+ is($_(7, "uname").value, "testuser", "Checking username 7");
+ todo_is($_(7, "qword").value, "", "Checking password 7 (q)");
+ is($_(7, "rword").value, "", "Checking password 7 (r)");
+ todo_is($_(7, "pword").value, "testpass", "Checking password 7");
+ // Check form 8
+ is($_(8, "uname").value, "testuser", "Checking username 8");
+ todo_is($_(8, "qword").value, "", "Checking password 8 (q)");
+ is($_(8, "rword").value, "", "Checking password 8 (r)");
+ todo_is($_(8, "pword").value, "testpass", "Checking password 8");
+ // Check form 9
+ is($_(9, "uname").value, "testuser", "Checking username 9");
+ todo_is($_(9, "qword").value, "", "Checking password 9 (q)");
+ is($_(9, "rword").value, "", "Checking password 9 (r)");
+ is($_(9, "pword").value, "testpass", "Checking password 9");
+
+ // TODO: as with the 2-password cases, add tests to check for creating new
+ // logins and changing passwords.
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
new file mode 100644
index 000000000..0eee8e696
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
@@ -0,0 +1,859 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: multiple login autocomplete
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+ // login0 has no username, so should be filtered out from the autocomplete list.
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "user0pass", "", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword");
+
+ var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword");
+
+ var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword");
+
+ // login 5 only used in the single-user forms
+ var login5 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete2", null,
+ "singleuser5", "singlepass5", "uname", "pword");
+
+ var login6A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user1", "form7pass1", "uname", "pword");
+ var login6B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user2", "form7pass2", "uname", "pword");
+
+ var login7 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete4", null,
+ "form8user", "form8pass", "uname", "pword");
+
+ var login8A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAB", "form9pass", "uname", "pword");
+ var login8B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAAB", "form9pass", "uname", "pword");
+ var login8C = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAABzz", "form9pass", "uname", "pword");
+
+ var login10 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete7", null,
+ "testuser10", "testpass10", "uname", "pword");
+
+
+ // try/catch in case someone runs the tests manually, twice.
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ Services.logins.addLogin(login4);
+ Services.logins.addLogin(login5);
+ Services.logins.addLogin(login6A);
+ Services.logins.addLogin(login6B);
+ Services.logins.addLogin(login7);
+ Services.logins.addLogin(login8A);
+ Services.logins.addLogin(login8B);
+ // login8C is added later
+ Services.logins.addLogin(login10);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+
+ addMessageListener("addLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to add is defined: " + loginVariableName);
+ Services.logins.addLogin(login);
+ });
+ addMessageListener("removeLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
+ Services.logins.removeLogin(login);
+ });
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- other forms test single logins, with autocomplete=off set -->
+ <form id="form2" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="http://autocomplete2" onsubmit="return false;" autocomplete="off">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form5" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- control -->
+ <form id="form6" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- This form will be manipulated to insert a different username field. -->
+ <form id="form7" action="http://autocomplete3" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for no autofill after onblur with blank username -->
+ <form id="form8" action="http://autocomplete4" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test autocomplete dropdown -->
+ <form id="form9" action="http://autocomplete5" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for onUsernameInput recipe testing -->
+ <form id="form11" action="http://autocomplete7" onsubmit="return false;">
+ <input type="text" name="1">
+ <input type="text" name="2">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- tests <form>-less autocomplete -->
+ <div id="form12">
+ <input type="text" name="uname" id="uname">
+ <input type="password" name="pword" id="pword">
+ <button type="submit">Submit</button>
+ </div>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function sendFakeAutocompleteEvent(element) {
+ var acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", false],
+ ["signon.autofillForms.http", true]]});
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form1_menuitems() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACForm("", "");
+});
+
+add_task(function* test_form1_first_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ doKey("down"); // first
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_second_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_third_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser3", "testpass3");
+});
+
+add_task(function* test_form1_fourth_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_first_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ yield spinEventLoop(); // let focus happen
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("down"); // deselects
+ doKey("down"); // first
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_wraparound_up_last_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("up"); // last (fourth)
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_down_up_up() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // select first entry
+ doKey("up"); // selects nothing!
+ doKey("up"); // select last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_up_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down");
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("up");
+ doKey("up");
+ doKey("up"); // first entry
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_fill_username_without_autofill_right() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // first
+ doKey("right");
+ yield spinEventLoop();
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_fill_username_without_autofill_left() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // first
+ doKey("left");
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_pageup_first() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry (page up)
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("page_up"); // first
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_pagedown_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ /* test 13 */
+ // Check last entry (page down)
+ doKey("down"); // first
+ doKey("page_down"); // last
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_untrusted_event() {
+ restoreForm();
+ yield spinEventLoop();
+
+ // Send a fake (untrusted) event.
+ checkACForm("", "");
+ uname.value = "zzzuser4";
+ sendFakeAutocompleteEvent(uname);
+ yield spinEventLoop();
+ checkACForm("zzzuser4", "");
+});
+
+add_task(function* test_form1_delete() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // XXX tried sending character "t" before/during dropdown to test
+ // filtering, but had no luck. Seemed like the character was getting lost.
+ // Setting uname.value didn't seem to work either. This works with a human
+ // driver, so I'm not sure what's up.
+
+ // Delete the first entry (of 4), "tempuser1"
+ doKey("down");
+ var numLogins;
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 5, "Correct number of logins before deleting one");
+
+ let countChangedPromise = notifyMenuChanged(3);
+ var deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 4, "Correct number of logins after deleting one");
+ yield countChangedPromise;
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_deletion() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check the new first entry (of 3)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_second() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Delete the second entry (of 3), "testuser3"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 3, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_first_after_deletion2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check the new first entry (of 2)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ /* test 54 */
+ // Delete the last entry (of 2), "zzzuser4"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 2, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_3_deletions() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check the only remaining entry
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_check_only_entry_remaining() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ /* test 56 */
+ // Delete the only remaining entry, "testuser2"
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 1, "Correct number of logins after deleting one");
+
+ // remove the login that's not shown in the list.
+ setupScript.sendSyncMessage("removeLogin", "login0");
+});
+
+/* Tests for single-user forms for ignoring autocomplete=off */
+add_task(function* test_form2() {
+ // Turn our attention to form2
+ uname = $_(2, "uname");
+ pword = $_(2, "pword");
+ checkACForm("singleuser5", "singlepass5");
+
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form3() {
+ uname = $_(3, "uname");
+ pword = $_(3, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form4() {
+ uname = $_(4, "uname");
+ pword = $_(4, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form5() {
+ uname = $_(5, "uname");
+ pword = $_(5, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6() {
+ // (this is a control, w/o autocomplete=off, to ensure the login
+ // that was being suppressed would have been filled in otherwise)
+ uname = $_(6, "uname");
+ pword = $_(6, "pword");
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6_changeUsername() {
+ // Test that the password field remains filled in after changing
+ // the username.
+ uname.focus();
+ doKey("right");
+ sendChar("X");
+ // Trigger the 'blur' event on uname
+ pword.focus();
+ yield spinEventLoop();
+ checkACForm("singleuser5X", "singlepass5");
+
+ setupScript.sendSyncMessage("removeLogin", "login5");
+});
+
+add_task(function* test_form7() {
+ uname = $_(7, "uname");
+ pword = $_(7, "pword");
+ checkACForm("", "");
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ var newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ pword.parentNode.insertBefore(newField, pword);
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+
+ // Delete login6B. It was created just to prevent filling in a login
+ // automatically, removing it makes it more likely that we'll catch a
+ // future regression with form filling here.
+ setupScript.sendSyncMessage("removeLogin", "login6B");
+});
+
+add_task(function* test_form7_2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ // The form changes, so we expect the old username field to get the
+ // selected autocomplete value, but neither the new username field nor
+ // the password field should have any values filled in.
+ yield spinEventLoop();
+ checkACForm("form7user1", "");
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+ restoreForm(); // clear field, so reloading test doesn't fail
+
+ setupScript.sendSyncMessage("removeLogin", "login6A");
+});
+
+add_task(function* test_form8() {
+ uname = $_(8, "uname");
+ pword = $_(8, "pword");
+ checkACForm("form8user", "form8pass");
+ restoreForm();
+});
+
+add_task(function* test_form8_blur() {
+ checkACForm("", "");
+ // Focus the previous form to trigger a blur.
+ $_(7, "uname").focus();
+});
+
+add_task(function* test_form8_2() {
+ checkACForm("", "");
+ restoreForm();
+});
+
+add_task(function* test_form8_3() {
+ checkACForm("", "");
+ setupScript.sendSyncMessage("removeLogin", "login7");
+});
+
+add_task(function* test_form9_filtering() {
+ // Turn our attention to form9 to test the dropdown - bug 497541
+ uname = $_(9, "uname");
+ pword = $_(9, "pword");
+ uname.focus();
+ let shownPromise = promiseACShown();
+ sendString("form9userAB");
+ yield shownPromise;
+
+ checkACForm("form9userAB", "");
+ uname.focus();
+ doKey("left");
+ shownPromise = promiseACShown();
+ sendChar("A");
+ let results = yield shownPromise;
+
+ checkACForm("form9userAAB", "");
+ checkArrayValues(results, ["form9userAAB"], "Check dropdown is updated after inserting 'A'");
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("form9userAAB", "form9pass");
+});
+
+add_task(function* test_form9_autocomplete_cache() {
+ // Note that this addLogin call will only be seen by the autocomplete
+ // attempt for the sendChar if we do not successfully cache the
+ // autocomplete results.
+ setupScript.sendSyncMessage("addLogin", "login8C");
+ uname.focus();
+ let promise0 = notifyMenuChanged(0);
+ sendChar("z");
+ yield promise0;
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup shouldn't open");
+
+ // check that empty results are cached - bug 496466
+ promise0 = notifyMenuChanged(0);
+ sendChar("z");
+ yield promise0;
+ popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup stays closed due to cached empty result");
+});
+
+add_task(function* test_form11_recipes() {
+ yield loadRecipes({
+ siteRecipes: [{
+ "hosts": ["mochi.test:8888"],
+ "usernameSelector": "input[name='1']",
+ "passwordSelector": "input[name='2']"
+ }],
+ });
+ uname = $_(11, "1");
+ pword = $_(11, "2");
+
+ // First test DOMAutocomplete
+ // Switch the password field to type=password so _fillForm marks the username
+ // field for autocomplete.
+ pword.type = "password";
+ yield promiseFormsProcessed();
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+
+ // Now test recipes with blur on the username field.
+ restoreForm();
+ checkACForm("", "");
+ uname.value = "testuser10";
+ checkACForm("testuser10", "");
+ doKey("tab");
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+ yield resetRecipes();
+});
+
+add_task(function* test_form12_formless() {
+ // Test form-less autocomplete
+ uname = $_(12, "uname");
+ pword = $_(12, "pword");
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Trigger autocomplete
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ let processedPromise = promiseFormsProcessed();
+ doKey("return"); // not "enter"!
+ yield processedPromise;
+ checkACForm("testuser", "testpass");
+});
+
+add_task(function* test_form12_open_on_trusted_focus() {
+ uname = $_(12, "uname");
+ pword = $_(12, "pword");
+ uname.value = "";
+ pword.value = "";
+
+ // Move focus to the password field so we can test the first click on the
+ // username field.
+ pword.focus();
+ checkACForm("", "");
+ const firePrivEventPromise = new Promise((resolve) => {
+ uname.addEventListener("click", (e) => {
+ ok(e.isTrusted, "Ensure event is trusted");
+ resolve();
+ });
+ });
+ const shownPromise = promiseACShown();
+ synthesizeMouseAtCenter(uname, {});
+ yield firePrivEventPromise;
+ yield shownPromise;
+ doKey("down");
+ const processedPromise = promiseFormsProcessed();
+ doKey("return"); // not "enter"!
+ yield processedPromise;
+ checkACForm("testuser", "testpass");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
new file mode 100644
index 000000000..40e322afd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for html5 input types (email, tel, url, etc.)</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: html5 input types (email, tel, url, etc.)
+<script>
+runChecksAfterCommonInit(() => startTest());
+
+runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login3 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://mochi.test:8888", "http://bug600551-1", null,
+ "testuser@example.com", "testpass1", "", "");
+ login2.init("http://mochi.test:8888", "http://bug600551-2", null,
+ "555-555-5555", "testpass2", "", "");
+ login3.init("http://mochi.test:8888", "http://bug600551-3", null,
+ "http://mozilla.org", "testpass3", "", "");
+ login4.init("http://mochi.test:8888", "http://bug600551-4", null,
+ "123456789", "testpass4", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2);
+ pwmgr.addLogin(login3);
+ pwmgr.addLogin(login4);
+});
+</script>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+ <form id="form1" action="http://bug600551-1">
+ <input type="email" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form2" action="http://bug600551-2">
+ <input type="tel" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://bug600551-3">
+ <input type="url" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="http://bug600551-4">
+ <input type="number" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- The following forms should not be filled with usernames -->
+ <form id="form5" action="formtest.js">
+ <input type="search" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form6" action="formtest.js">
+ <input type="datetime" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form7" action="formtest.js">
+ <input type="date" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form8" action="formtest.js">
+ <input type="month" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form9" action="formtest.js">
+ <input type="week" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form10" action="formtest.js">
+ <input type="time" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form11" action="formtest.js">
+ <input type="datetime-local" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form12" action="formtest.js">
+ <input type="range" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form13" action="formtest.js">
+ <input type="color" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 600551
+ (Password manager not working with input type=email)
+ */
+function startTest() {
+ checkForm(1, "testuser@example.com", "testpass1");
+ checkForm(2, "555-555-5555", "testpass2");
+ checkForm(3, "http://mozilla.org", "testpass3");
+ checkForm(4, "123456789", "testpass4");
+
+ is($_(5, "uname").value, "", "type=search should not be considered a username");
+
+ is($_(6, "uname").value, "", "type=datetime should not be considered a username");
+
+ is($_(7, "uname").value, "", "type=date should not be considered a username");
+
+ is($_(8, "uname").value, "", "type=month should not be considered a username");
+
+ is($_(9, "uname").value, "", "type=week should not be considered a username");
+
+ is($_(10, "uname").value, "", "type=time should not be considered a username");
+
+ is($_(11, "uname").value, "", "type=datetime-local should not be considered a username");
+
+ is($_(12, "uname").value, "50", "type=range should not be considered a username");
+
+ is($_(13, "uname").value, "#000000", "type=color should not be considered a username");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html
new file mode 100644
index 000000000..e0a2883c8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=355063
+-->
+<head>
+ <meta charset="utf-8"/>
+ <title>Test for Bug 355063</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 355063 **/
+
+ runChecksAfterCommonInit(function startTest() {
+ info("startTest");
+ // Password Manager's own listener should always have been added first, so
+ // the test's listener should be called after the pwmgr's listener fills in
+ // a login.
+ //
+ SpecialPowers.addChromeEventListener("DOMFormHasPassword", function eventFired() {
+ SpecialPowers.removeChromeEventListener("DOMFormHasPassword", eventFired);
+ var passField = $("p1");
+ passField.addEventListener("input", checkForm);
+ });
+ addForm();
+ });
+
+ function addForm() {
+ info("addForm");
+ var c = document.getElementById("content");
+ c.innerHTML = "<form id=form1>form1: <input id=u1><input type=password id=p1></form><br>";
+ }
+
+ function checkForm() {
+ info("checkForm");
+ var userField = document.getElementById("u1");
+ var passField = document.getElementById("p1");
+ is(userField.value, "testuser", "checking filled username");
+ is(passField.value, "testpass", "checking filled password");
+
+ SimpleTest.finish();
+ }
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=355063">Mozilla Bug 355063</a>
+<p id="display"></p>
+<div id="content">
+forms go here!
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
new file mode 100644
index 000000000..40fec8c46
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
@@ -0,0 +1,213 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms and logins without a username</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms and logins without a username.
+<script>
+runChecksAfterCommonInit(() => startTest());
+runInParent(() => {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ var pwmgr = Cc["@mozilla.org/login-manager;1"]
+ .getService(Ci.nsILoginManager);
+
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
+
+ // pwlogin1 uses a unique formSubmitURL, to check forms where no other logins
+ // will apply. pwlogin2 uses the normal formSubmitURL, so that we can test
+ // forms with a mix of username and non-username logins that might apply.
+ //
+ // Note: pwlogin2 is deleted at the end of the test.
+
+ pwlogin1 = new nsLoginInfo();
+ pwlogin2 = new nsLoginInfo();
+
+ pwlogin1.init("http://mochi.test:8888", "http://mochi.test:1111", null,
+ "", "1234", "uname", "pword");
+
+ pwlogin2.init("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "", "1234", "uname", "pword");
+
+
+ pwmgr.addLogin(pwlogin1);
+ pwmgr.addLogin(pwlogin2);
+});
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+
+<!-- simple form: no username field, 1 password field -->
+<form id='form1' action='http://mochi.test:1111/formtest.js'> 1
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- simple form: no username field, 2 password fields -->
+<form id='form2' action='http://mochi.test:1111/formtest.js'> 2
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- simple form: no username field, 3 password fields -->
+<form id='form3' action='http://mochi.test:1111/formtest.js'> 3
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+ <input type='password' name='pname3' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 4 password fields, should be ignored. -->
+<form id='form4' action='http://mochi.test:1111/formtest.js'> 4
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+ <input type='password' name='pname3' value=''>
+ <input type='password' name='pname4' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+<!-- 1 username field -->
+<form id='form5' action='http://mochi.test:1111/formtest.js'> 5
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+<!-- 1 username field, with a value set -->
+<form id='form6' action='http://mochi.test:1111/formtest.js'> 6
+ <input type='text' name='uname' value='someuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+<!--
+(The following forms have 2 potentially-matching logins, on is
+password-only, the other is username+password)
+-->
+
+
+
+<!-- 1 username field, with value set. Fill in the matching U+P login -->
+<form id='form7' action='formtest.js'> 7
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, with value set. Don't fill in U+P login-->
+<form id='form8' action='formtest.js'> 8
+ <input type='text' name='uname' value='someuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+<!-- 1 username field, too small for U+P login -->
+<form id='form9' action='formtest.js'> 9
+ <input type='text' name='uname' value='' maxlength="4">
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for U+P login -->
+<form id='form10' action='formtest.js'> 10
+ <input type='text' name='uname' value='' maxlength="0">
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for U+P login -->
+<form id='form11' action='formtest.js'> 11
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="4">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for either login -->
+<form id='form12' action='formtest.js'> 12
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="1">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for either login -->
+<form id='form13' action='formtest.js'> 13
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="0">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: password-only logins **/
+function startTest() {
+
+ checkForm(1, "1234");
+ checkForm(2, "1234", "");
+ checkForm(3, "1234", "", "");
+ checkUnmodifiedForm(4);
+
+ checkForm(5, "", "1234");
+ checkForm(6, "someuser", "");
+
+ checkForm(7, "testuser", "testpass");
+ checkForm(8, "someuser", "");
+
+ checkForm(9, "", "1234");
+ checkForm(10, "", "1234");
+ checkForm(11, "", "1234");
+
+ checkUnmodifiedForm(12);
+ checkUnmodifiedForm(13);
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
new file mode 100644
index 000000000..ad4a41cdb
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test bug 627616 related to proxy authentication</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var Ci = SpecialPowers.Ci;
+
+ function makeXHR(expectedStatus, expectedText, extra) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "authenticate.sjs?" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm&" +
+ "user=user1name&" +
+ "pass=user1pass&" +
+ "realm=mochirealm&" +
+ extra || "");
+ xhr.onloadend = function() {
+ is(xhr.status, expectedStatus, "xhr.status");
+ is(xhr.statusText, expectedText, "xhr.statusText");
+ runNextTest();
+ };
+ return xhr;
+ }
+
+ function testNonAnonymousCredentials() {
+ var xhr = makeXHR(200, "OK");
+ xhr.send();
+ }
+
+ function testAnonymousCredentials() {
+ // Test that an anonymous request correctly performs proxy authentication
+ var xhr = makeXHR(401, "Authentication required");
+ SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
+ xhr.send();
+ }
+
+ function testAnonymousNoAuth() {
+ // Next, test that an anonymous request still does not include any non-proxy
+ // authentication headers.
+ var xhr = makeXHR(200, "Authorization header not found", "anonymous=1");
+ SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
+ xhr.send();
+ }
+
+ var gExpectedDialogs = 0;
+ var gCurrentTest;
+ function runNextTest() {
+ is(gExpectedDialogs, 0, "received expected number of auth dialogs");
+ mm.sendAsyncMessage("prepareForNextTest");
+ mm.addMessageListener("prepareForNextTestDone", function prepared(msg) {
+ mm.removeMessageListener("prepareForNextTestDone", prepared);
+ if (pendingTests.length > 0) {
+ ({expectedDialogs: gExpectedDialogs,
+ test: gCurrentTest} = pendingTests.shift());
+ gCurrentTest.call(this);
+ } else {
+ mm.sendAsyncMessage("cleanup");
+ mm.addMessageListener("cleanupDone", () => {
+ // mm.destroy() is called as a cleanup function by runInParent(), no
+ // need to do it here.
+ SimpleTest.finish();
+ });
+ }
+ });
+ }
+
+ var pendingTests = [{expectedDialogs: 2, test: testNonAnonymousCredentials},
+ {expectedDialogs: 1, test: testAnonymousCredentials},
+ {expectedDialogs: 0, test: testAnonymousNoAuth}];
+
+ let mm = runInParent(() => {
+ const { classes: parentCc, interfaces: parentCi, utils: parentCu } = Components;
+
+ parentCu.import("resource://gre/modules/Services.jsm");
+ parentCu.import("resource://gre/modules/NetUtil.jsm");
+ parentCu.import("resource://gre/modules/Timer.jsm");
+ parentCu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ let channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true
+ });
+
+ let pps = parentCc["@mozilla.org/network/protocol-proxy-service;1"].
+ getService(parentCi.nsIProtocolProxyService);
+ pps.asyncResolve(channel, 0, {
+ onProxyAvailable(req, uri, pi, status) {
+ let mozproxy = "moz-proxy://" + pi.host + ":" + pi.port;
+ let login = parentCc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(parentCi.nsILoginInfo);
+ login.init(mozproxy, null, "proxy_realm", "proxy_user", "proxy_pass",
+ "", "");
+ Services.logins.addLogin(login);
+
+ let login2 = parentCc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(parentCi.nsILoginInfo);
+ login2.init("http://mochi.test:8888", null, "mochirealm", "user1name",
+ "user1pass", "", "");
+ Services.logins.addLogin(login2);
+
+ sendAsyncMessage("setupDone");
+ },
+ QueryInterface: XPCOMUtils.generateQI([parentCi.nsIProtocolProxyCallback]),
+ });
+
+ addMessageListener("prepareForNextTest", message => {
+ parentCc["@mozilla.org/network/http-auth-manager;1"].
+ getService(parentCi.nsIHttpAuthManager).
+ clearAll();
+ sendAsyncMessage("prepareForNextTestDone");
+ });
+
+ let dialogObserverTopic = "common-dialog-loaded";
+
+ function dialogObserver(subj, topic, data) {
+ subj.Dialog.ui.prompt.document.documentElement.acceptDialog();
+ sendAsyncMessage("promptAccepted");
+ }
+
+ Services.obs.addObserver(dialogObserver, dialogObserverTopic, false);
+
+ addMessageListener("cleanup", message => {
+ Services.obs.removeObserver(dialogObserver, dialogObserverTopic);
+ sendAsyncMessage("cleanupDone");
+ });
+ });
+
+ mm.addMessageListener("promptAccepted", msg => {
+ gExpectedDialogs--;
+ });
+ mm.addMessageListener("setupDone", msg => {
+ runNextTest();
+ });
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
new file mode 100644
index 000000000..4ad08bee2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=776171
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 776171 related to HTTP auth</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="startTest()">
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test checks we correctly ignore authentication entry
+ * for a subpath and use creds from the URL when provided when XHR
+ * is used with filled user name and password.
+ *
+ * 1. connect auth2/authenticate.sjs that expects user1:pass1 password
+ * 2. connect a dummy URL at the same path
+ * 3. connect authenticate.sjs that again expects user1:pass1 password
+ * in this case, however, we have an entry without an identity
+ * for this path (that is a parent for auth2 path in the first step)
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function doxhr(URL, user, pass, next) {
+ var xhr = new XMLHttpRequest();
+ if (user && pass)
+ xhr.open("POST", URL, true, user, pass);
+ else
+ xhr.open("POST", URL, true);
+ xhr.onload = function() {
+ is(xhr.status, 200, "Got status 200");
+ next();
+ };
+ xhr.onerror = function() {
+ ok(false, "request passed");
+ finishTest();
+ };
+ xhr.send();
+}
+
+function startTest() {
+ doxhr("auth2/authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", function() {
+ doxhr("auth2", null, null, function() {
+ doxhr("authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", SimpleTest.finish);
+ });
+ });
+}
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
new file mode 100644
index 000000000..316f59da7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete due to multiple matching logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: autocomplete due to multiple matching logins
+
+<script>
+runChecksAfterCommonInit(false);
+
+SpecialPowers.loadChromeScript(function addLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "name", "pass", "uname", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "Name", "Pass", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "USER", "PASS", "uname", "pword");
+
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: autocomplete due to multiple matching logins **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username");
+ is(pword.value, expectedPassword, "Checking " + formID + " password");
+}
+
+add_task(function* test_empty_first_entry() {
+ /* test 1 */
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ // Trigger autocomplete popup
+ restoreForm();
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+ let shownPromise = promiseACShown();
+ doKey("down");
+ let results = yield shownPromise;
+ popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected");
+ checkArrayValues(results, ["name", "Name", "USER"], "initial");
+
+ // Check first entry
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name", "pass");
+});
+
+add_task(function* test_empty_second_entry() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("Name", "Pass");
+});
+
+add_task(function* test_empty_third_entry() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("USER", "PASS");
+});
+
+add_task(function* test_preserve_matching_username_case() {
+ restoreForm();
+ uname.value = "user";
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check that we don't clobber user-entered text when tabbing away
+ // (even with no autocomplete entry selected)
+ doKey("tab");
+ yield promiseFormsProcessed();
+ checkACForm("user", "PASS");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html
new file mode 100644
index 000000000..430081b3a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for considering form action</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 360493
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+ <!-- normal form with normal relative action. -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL -->
+ <form id="form2" action="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL, and change the path -->
+ <form id="form3" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL, and change the path and filename -->
+ <form id="form4" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/not_a_test.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- specify the action URL relative to the current document-->
+ <form id="form5" action="./formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- specify the action URL relative to the current server -->
+ <form id="form6" action="/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change the method from get to post -->
+ <form id="form7" action="formtest.js" method="POST">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Blank action URL specified -->
+ <form id="form8" action="">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- |action| attribute entirely missing -->
+ <form id="form9" >
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- action url as javascript -->
+ <form id="form10" action="javascript:alert('this form is not submitted so this alert should not be invoked');">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- TODO: action=IP.ADDRESS instead of HOSTNAME? -->
+ <!-- TODO: test with |base href="http://othersite//"| ? -->
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 360493 (Cross-Site Forms + Password
+ Manager = Security Failure) **/
+
+// This test is designed to make sure variations on the form's |action|
+// and |method| continue to work with the fix for 360493.
+
+function startTest() {
+ for (var i = 1; i <= 9; i++) {
+ // Check form i
+ is($_(i, "uname").value, "testuser", "Checking for filled username " + i);
+ is($_(i, "pword").value, "testpass", "Checking for filled password " + i);
+ }
+
+ // The login's formSubmitURL isn't "javascript:", so don't fill it in.
+ isnot($_(10, "uname"), "testuser", "Checking username w/ JS action URL");
+ isnot($_(10, "pword"), "testpass", "Checking password w/ JS action URL");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html
new file mode 100644
index 000000000..0f0056de0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for considering form action</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 360493
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+ <!-- The tests in this page exercise things that shouldn't work. -->
+
+ <!-- Change port # of action URL from 8888 to 7777 -->
+ <form id="form1" action="http://localhost:7777/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- No port # in action URL -->
+ <form id="form2" action="http://localhost/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change protocol from http:// to ftp://, include the expected 8888 port # -->
+ <form id="form3" action="ftp://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change protocol from http:// to ftp://, no port # specified -->
+ <form id="form4" action="ftp://localhost/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. -->
+ <form id="form5" action="about:blank">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. (If the normal embedded action URL doesn't work, that should mean other URLs won't either) -->
+ <form id="form6" action="view-source:http://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. -->
+ <form id="form7" action="view-source:formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Action URL points to a different host (this is the archetypical exploit) -->
+ <form id="form8" action="http://www.cnn.com/">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Action URL points to a different host, user field prefilled -->
+ <form id="form9" action="http://www.cnn.com/">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try wrapping a evil form around a good form, to see if we can confuse the parser. -->
+ <form id="form10-A" action="http://www.cnn.com/">
+ <form id="form10-B" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit (inner)</button>
+ <button type="reset"> Reset (inner)</button>
+ </form>
+ <button type="submit" id="neutered_submit10">Submit (outer)</button>
+ <button type="reset">Reset (outer)</button>
+ </form>
+
+ <!-- Try wrapping a good form around an evil form, to see if we can confuse the parser. -->
+ <form id="form11-A" action="formtest.js">
+ <form id="form11-B" action="http://www.cnn.com/">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit (inner)</button>
+ <button type="reset"> Reset (inner)</button>
+ </form>
+ <button type="submit" id="neutered_submit11">Submit (outer)</button>
+ <button type="reset">Reset (outer)</button>
+ </form>
+
+<!-- TODO: probably should have some accounts which have no port # in the action url. JS too. And different host/proto. -->
+<!-- TODO: www.site.com vs. site.com? -->
+<!-- TODO: foo.site.com vs. bar.site.com? -->
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 360493 (Cross-Site Forms + Password Manager = Security Failure) **/
+
+function startTest() {
+ for (var i = 1; i <= 8; i++) {
+ // Check form i
+ is($_(i, "uname").value, "", "Checking for unfilled username " + i);
+ is($_(i, "pword").value, "", "Checking for unfilled password " + i);
+ }
+
+ is($_(9, "uname").value, "testuser", "Checking for unmodified username 9");
+ is($_(9, "pword").value, "", "Checking for unfilled password 9");
+
+ is($_("10-A", "uname").value, "", "Checking for unfilled username 10A");
+ is($_("10-A", "pword").value, "", "Checking for unfilled password 10A");
+
+ // The DOM indicates this form could be filled, as the evil inner form
+ // is discarded. And yet pwmgr seems not to fill it. Not sure why.
+ todo(false, "Mangled form combo not being filled when maybe it could be?");
+ is($_("11-A", "uname").value, "testuser", "Checking filled username 11A");
+ is($_("11-A", "pword").value, "testpass", "Checking filled password 11A");
+
+ // Verify this by making sure there are no extra forms in the document, and
+ // that the submit button for the neutered forms don't do anything.
+ // If the test finds extra forms the submit() causes the test to timeout, then
+ // there may be a security issue.
+ is(document.forms.length, 11, "Checking for unexpected forms");
+ $("neutered_submit10").click();
+ $("neutered_submit11").click();
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html
new file mode 100644
index 000000000..d37e92c40
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with a JS submit action</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: form with JS submit action
+<script>
+runChecksAfterCommonInit(() => startTest());
+
+runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let jslogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ jslogin.init("http://mochi.test:8888", "javascript:", null,
+ "jsuser", "jspass123", "uname", "pword");
+ Services.logins.addLogin(jslogin);
+});
+
+/** Test for Login Manager: JS action URL **/
+
+function startTest() {
+ checkForm(1, "jsuser", "jspass123");
+
+ SimpleTest.finish();
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+
+<form id='form1' action='javascript:alert("never shows")'> 1
+ <input name="uname">
+ <input name="pword" type="password">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
new file mode 100644
index 000000000..6263c818d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofilling of fields outside of a form</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ // Tell the parent to setup test logins.
+ chromeScript.sendAsyncMessage("setupParent", { selfFilling: true });
+ });
+});
+
+let doneSetupPromise = new Promise(resolve => {
+ // When the setup is done, load a recipe for this test.
+ chromeScript.addMessageListener("doneSetup", function doneSetup() {
+ resolve();
+ });
+});
+
+add_task(function* setup() {
+ info("Waiting for loads and setup");
+ yield doneSetupPromise;
+
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+
+const DEFAULT_ORIGIN = "http://mochi.test:8888";
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password>`,
+
+ // Expected outputs
+ expectedInputValues: ["testpass"],
+ },
+ {
+ document: `<input>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass"],
+ },
+ {
+ document: `<input>
+ <input type=password>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass", ""],
+ },
+ {
+ document: `<input>
+ <input type=password>
+ <input type=password>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass", "", ""],
+ },
+ {
+ document: `<input>
+ <input type=password form="form1">
+ <input type=password>
+ <form id="form1">
+ <input>
+ <input type=password>
+ </form>`,
+ expectedFormCount: 2,
+ expectedInputValues: ["testuser", "testpass", "testpass", "", ""],
+ },
+ {
+ document: `<!-- formless password field selector recipe test -->
+ <input>
+ <input type=password>
+ <input>
+ <input type=password name="recipepword">`,
+ expectedInputValues: ["", "", "testuser", "testpass"],
+ },
+ {
+ document: `<!-- formless username and password field selector recipe test -->
+ <input name="recipeuname">
+ <input>
+ <input type=password>
+ <input type=password name="recipepword">`,
+ expectedInputValues: ["testuser", "", "", "testpass"],
+ },
+ {
+ document: `<!-- form and formless recipe field selector test -->
+ <input name="recipeuname">
+ <input>
+ <input type=password form="form1"> <!-- not filled since recipe affects both FormLikes -->
+ <input type=password>
+ <input type=password name="recipepword">
+ <form id="form1">
+ <input>
+ <input type=password>
+ </form>`,
+ expectedFormCount: 2,
+ expectedInputValues: ["testuser", "", "", "", "testpass", "", ""],
+ },
+];
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+ let frameDoc = loginFrame.contentWindow.document;
+
+ for (let tc of TESTCASES) {
+ info("Starting testcase: " + JSON.stringify(tc));
+
+ let numFormLikesExpected = tc.expectedFormCount || 1;
+
+ let processedFormPromise = promiseFormsProcessed(numFormLikesExpected);
+
+ frameDoc.documentElement.innerHTML = tc.document;
+ info("waiting for " + numFormLikesExpected + " processed form(s)");
+ yield processedFormPromise;
+
+ let testInputs = frameDoc.documentElement.querySelectorAll("input");
+ is(testInputs.length, tc.expectedInputValues.length, "Check number of inputs");
+ for (let i = 0; i < tc.expectedInputValues.length; i++) {
+ let expectedValue = tc.expectedInputValues[i];
+ is(testInputs[i].value, expectedValue,
+ "Check expected input value " + i + ": " + expectedValue);
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
new file mode 100644
index 000000000..468da1e7f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test capturing of fields outside of a form</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+add_task(function* setup() {
+ info("Waiting for page and frame loads");
+ yield loadPromise;
+
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+const DEFAULT_ORIGIN = "http://mochi.test:8888";
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password value="pass1">`,
+ inputIndexForFormLike: 0,
+
+ // Expected outputs similar to RemoteLogins:onFormSubmit
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">`,
+ inputIndexForFormLike: 0,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">`,
+ inputIndexForFormLike: 1,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">`,
+ inputIndexForFormLike: 2,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">
+ <input type=password value="pass2">`,
+ inputIndexForFormLike: 3,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="user2" form="form1">
+ <input type=password value="pass1">
+ <form id="form1">
+ <input value="user3">
+ <input type=password value="pass2">
+ </form>`,
+ inputIndexForFormLike: 2,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<!-- recipe field override -->
+ <input name="recipeuname" value="username from recipe">
+ <input value="default field username">
+ <input type=password value="pass1">
+ <input name="recipepword" type=password value="pass2">`,
+ inputIndexForFormLike: 2,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "username from recipe",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+];
+
+function getSubmitMessage() {
+ info("getSubmitMessage");
+ return new Promise((resolve, reject) => {
+ chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+ info("got formSubmissionProcessed");
+ chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+ resolve(...args);
+ });
+ });
+}
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+ let frameDoc = loginFrame.contentWindow.document;
+
+ for (let tc of TESTCASES) {
+ info("Starting testcase: " + JSON.stringify(tc));
+ frameDoc.documentElement.innerHTML = tc.document;
+ let inputForFormLike = frameDoc.querySelectorAll("input")[tc.inputIndexForFormLike];
+
+ let formLike = LoginFormFactory.createFromField(inputForFormLike);
+
+ info("Calling _onFormSubmit with FormLike");
+ let processedPromise = getSubmitMessage();
+ LoginManagerContent._onFormSubmit(formLike);
+
+ let submittedResult = yield processedPromise;
+
+ // Check data sent via RemoteLogins:onFormSubmit
+ is(submittedResult.hostname, tc.hostname, "Check hostname");
+ is(submittedResult.formSubmitURL, tc.formSubmitURL, "Check formSubmitURL");
+
+ if (tc.usernameFieldValue === null) {
+ is(submittedResult.usernameField, tc.usernameFieldValue, "Check usernameField");
+ } else {
+ is(submittedResult.usernameField.value, tc.usernameFieldValue, "Check usernameField");
+ }
+
+ is(submittedResult.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
+
+ if (tc.oldPasswordFieldValue === null) {
+ is(submittedResult.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ } else {
+ is(submittedResult.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
new file mode 100644
index 000000000..b07d0886c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test capturing of fields outside of a form due to navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.formlessCapture.enabled", true],
+ ],
+ });
+
+ info("Waiting for page and frame loads");
+ yield loadPromise;
+
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["test1.mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+};
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password value="pass1">`,
+
+ // Expected outputs similar to RemoteLogins:onFormSubmit
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">
+ <input type=password value="pass2">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="user2" form="form1">
+ <input type=password value="pass1">
+ <form id="form1">
+ <input value="user3">
+ <input type=password value="pass2">
+ </form>`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<!-- recipe field override -->
+ <input name="recipeuname" value="username from recipe">
+ <input value="default field username">
+ <input type=password value="pass1">
+ <input name="recipepword" type=password value="pass2">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "username from recipe",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+];
+
+function getSubmitMessage() {
+ info("getSubmitMessage");
+ return new Promise((resolve, reject) => {
+ chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+ info("got formSubmissionProcessed");
+ chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+ resolve(...args);
+ });
+ });
+}
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+
+ for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc));
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function frameLoaded() {
+ loginFrame.removeEventListener("load", frameLoaded);
+ resolve();
+ });
+ });
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ yield loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+ frameDoc.documentElement.innerHTML = tc.document;
+ // Wait for the form to be processed before trying to submit.
+ yield promiseFormsProcessed();
+ let processedPromise = getSubmitMessage();
+ info("Running " + scriptName + " script to cause a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ let submittedResult = yield processedPromise;
+
+ // Check data sent via RemoteLogins:onFormSubmit
+ is(submittedResult.hostname, tc.hostname, "Check hostname");
+ is(submittedResult.formSubmitURL, tc.formSubmitURL, "Check formSubmitURL");
+
+ if (tc.usernameFieldValue === null) {
+ is(submittedResult.usernameField, tc.usernameFieldValue, "Check usernameField");
+ } else {
+ is(submittedResult.usernameField.value, tc.usernameFieldValue, "Check usernameField");
+ }
+
+ is(submittedResult.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
+
+ if (tc.oldPasswordFieldValue === null) {
+ is(submittedResult.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ } else {
+ is(submittedResult.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ }
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
new file mode 100644
index 000000000..4283f128c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test no capturing of fields outside of a form due to navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+
+SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+function submissionProcessed(...args) {
+ ok(false, "No formSubmissionProcessed should occur in this test");
+ info("got: " + JSON.stringify(args));
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.formlessCapture.enabled", true],
+ ],
+ });
+
+ info("Waiting for page and frame loads");
+ yield loadPromise;
+
+ chromeScript.addMessageListener("formSubmissionProcessed", submissionProcessed);
+
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.removeMessageListener("formSubmissionProcessed", submissionProcessed);
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+ WINDOW_LOCATION_RELOAD: `window.location.reload();`,
+ HISTORY_BACK: `history.back();`,
+ HISTORY_GO_MINUS1: `history.go(-1);`,
+};
+const TESTCASES = [
+ // Begin test cases that shouldn't trigger capture.
+ {
+ // For now we don't trigger upon navigation if <form> is used.
+ document: `<form><input type=password value="pass1"></form>`,
+ },
+ {
+ // Empty password field
+ document: `<input type=password value="">`,
+ },
+ {
+ // Test with an input that would normally be captured but with SCRIPTS that
+ // shouldn't trigger capture.
+ document: `<input type=password value="pass2">`,
+ wouldCapture: true,
+ },
+];
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+
+ for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ if (tc.wouldCapture && ["PUSHSTATE", "WINDOW_LOCATION"].includes(scriptName)) {
+ // Don't run scripts that should actually capture for this testcase.
+ continue;
+ }
+
+ info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc));
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function frameLoaded() {
+ loginFrame.removeEventListener("load", frameLoaded);
+ resolve();
+ });
+ });
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ yield loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+ frameDoc.documentElement.innerHTML = tc.document;
+
+ // Wait for the form to be processed before trying to submit.
+ yield promiseFormsProcessed();
+
+ info("Running " + scriptName + " script to check for a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ // Wait for 5000ms to see if the promise above resolves.
+ yield new Promise(resolve => setTimeout(resolve, 5000));
+ ok(true, "Done waiting for captures");
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
new file mode 100644
index 000000000..0e77956d8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for input events in Login Manager</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="onNewEvent(event)">
+Login Manager test: input events should fire.
+
+<script>
+runChecksAfterCommonInit();
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+/** Test for Login Manager: form fill, should get input events. **/
+
+var usernameInputFired = false;
+var passwordInputFired = false;
+var usernameChangeFired = false;
+var passwordChangeFired = false;
+var onloadFired = false;
+
+function onNewEvent(e) {
+ info("Got " + e.type + " event.");
+ if (e.type == "load") {
+ onloadFired = true;
+ } else if (e.type == "input") {
+ if (e.target.name == "uname") {
+ is(e.target.value, "testuser", "Should get 'testuser' as username");
+ ok(!usernameInputFired, "Should not have gotten an input event for the username field yet.");
+ usernameInputFired = true;
+ } else if (e.target.name == "pword") {
+ is(e.target.value, "testpass", "Should get 'testpass' as password");
+ ok(!passwordInputFired, "Should not have gotten an input event for the password field yet.");
+ passwordInputFired = true;
+ }
+ } else if (e.type == "change") {
+ if (e.target.name == "uname") {
+ is(e.target.value, "testuser", "Should get 'testuser' as username");
+ ok(usernameInputFired, "Should get input event before change event for username field.");
+ ok(!usernameChangeFired, "Should not have gotten a change event for the username field yet.");
+ usernameChangeFired = true;
+ } else if (e.target.name == "pword") {
+ is(e.target.value, "testpass", "Should get 'testpass' as password");
+ ok(passwordInputFired, "Should get input event before change event for password field.");
+ ok(!passwordChangeFired, "Should not have gotten a change event for the password field yet.");
+ passwordChangeFired = true;
+ }
+ }
+ if (onloadFired && usernameInputFired && passwordInputFired && usernameChangeFired && passwordChangeFired) {
+ ok(true, "All events fired as expected, we're done.");
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.registerCleanupFunction(function cleanup() {
+ clearTimeout(timeout);
+ $_(1, "uname").removeAttribute("oninput");
+ $_(1, "pword").removeAttribute("oninput");
+ $_(1, "uname").removeAttribute("onchange");
+ $_(1, "pword").removeAttribute("onchange");
+ document.body.removeAttribute("onload");
+});
+
+var timeout = setTimeout(function() {
+ ok(usernameInputFired, "Username input event should have fired by now.");
+ ok(passwordInputFired, "Password input event should have fired by now.");
+ ok(usernameChangeFired, "Username change event should have fired by now.");
+ ok(passwordChangeFired, "Password change event should have fired by now.");
+ ok(onloadFired, "Window load event should have fired by now.");
+ ok(false, "Not all events fired yet.");
+ SimpleTest.finish();
+}, 10000);
+
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input type="text" name="uname" oninput="onNewEvent(event)" onchange="onNewEvent(event)">
+ <input type="password" name="pword" oninput="onNewEvent(event)" onchange="onNewEvent(event)">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
new file mode 100644
index 000000000..d058a87f9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for input events in Login Manager when username/password are filled in already</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="onNewEvent(event)">
+Login Manager test: input events should fire.
+
+<script>
+runChecksAfterCommonInit();
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+/** Test for Login Manager: form fill when form is already filled, should not get input events. **/
+
+var onloadFired = false;
+
+function onNewEvent(e) {
+ console.error("Got " + e.type + " event.");
+ if (e.type == "load") {
+ onloadFired = true;
+ $_(1, "uname").focus();
+ sendKey("Tab");
+ } else {
+ ok(false, "Got an input event for " + e.target.name + " field, which shouldn't happen.");
+ }
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input type="text" name="uname" oninput="onNewEvent(event)" value="testuser">
+ <input type="password" name="pword" oninput="onNewEvent(event)" onfocus="setTimeout(function() { SimpleTest.finish() }, 1000);" value="testpass">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
new file mode 100644
index 000000000..c5d0a44fa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
@@ -0,0 +1,861 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test insecure form field autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+ // login0 has no username, so should be filtered out from the autocomplete list.
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "user0pass", "", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword");
+
+ var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword");
+
+ var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword");
+
+ // login 5 only used in the single-user forms
+ var login5 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete2", null,
+ "singleuser5", "singlepass5", "uname", "pword");
+
+ var login6A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user1", "form7pass1", "uname", "pword");
+ var login6B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user2", "form7pass2", "uname", "pword");
+
+ var login7 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete4", null,
+ "form8user", "form8pass", "uname", "pword");
+
+ var login8A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAB", "form9pass", "uname", "pword");
+ var login8B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAAB", "form9pass", "uname", "pword");
+ var login8C = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAABzz", "form9pass", "uname", "pword");
+
+ var login10 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete7", null,
+ "testuser10", "testpass10", "uname", "pword");
+
+
+ // try/catch in case someone runs the tests manually, twice.
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ Services.logins.addLogin(login4);
+ Services.logins.addLogin(login5);
+ Services.logins.addLogin(login6A);
+ Services.logins.addLogin(login6B);
+ Services.logins.addLogin(login7);
+ Services.logins.addLogin(login8A);
+ Services.logins.addLogin(login8B);
+ // login8C is added later
+ Services.logins.addLogin(login10);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+
+ addMessageListener("addLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to add is defined: " + loginVariableName);
+ Services.logins.addLogin(login);
+ });
+ addMessageListener("removeLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
+ Services.logins.removeLogin(login);
+ });
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- other forms test single logins, with autocomplete=off set -->
+ <form id="form2" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="http://autocomplete2" onsubmit="return false;" autocomplete="off">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form5" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- control -->
+ <form id="form6" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- This form will be manipulated to insert a different username field. -->
+ <form id="form7" action="http://autocomplete3" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for no autofill after onblur with blank username -->
+ <form id="form8" action="http://autocomplete4" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test autocomplete dropdown -->
+ <form id="form9" action="http://autocomplete5" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for onUsernameInput recipe testing -->
+ <form id="form11" action="http://autocomplete7" onsubmit="return false;">
+ <input type="text" name="1">
+ <input type="text" name="2">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- tests <form>-less autocomplete -->
+ <div id="form12">
+ <input type="text" name="uname" id="uname">
+ <input type="password" name="pword" id="pword">
+ <button type="submit">Submit</button>
+ </div>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function sendFakeAutocompleteEvent(element) {
+ var acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]]});
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form1_warning_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised. Learn More",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select insecure warning
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACForm("", "");
+});
+
+add_task(function* test_form1_first_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_second_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_third_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser3", "testpass3");
+});
+
+add_task(function* test_form1_fourth_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_first_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ yield spinEventLoop(); // let focus happen
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("down"); // deselects
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_wraparound_up_last_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("up"); // last (fourth)
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_down_up_up() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // select first entry
+ doKey("up"); // selects nothing!
+ doKey("up"); // select last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_up_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down");
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("up");
+ doKey("up");
+ doKey("up"); // skip insecure warning
+ doKey("up"); // first entry
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_fill_username_without_autofill_right() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("right");
+ yield spinEventLoop();
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_fill_username_without_autofill_left() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("left");
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_pageup_first() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry (page up)
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("page_up"); // first
+ doKey("down"); // skip insecure warning
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_pagedown_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // test 13
+ // Check last entry (page down)
+ doKey("down"); // first
+ doKey("page_down"); // last
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_untrusted_event() {
+ restoreForm();
+ yield spinEventLoop();
+
+ // Send a fake (untrusted) event.
+ checkACForm("", "");
+ uname.value = "zzzuser4";
+ sendFakeAutocompleteEvent(uname);
+ yield spinEventLoop();
+ checkACForm("zzzuser4", "");
+});
+
+add_task(function* test_form1_delete() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // XXX tried sending character "t" before/during dropdown to test
+ // filtering, but had no luck. Seemed like the character was getting lost.
+ // Setting uname.value didn't seem to work either. This works with a human
+ // driver, so I'm not sure what's up.
+
+ doKey("down"); // skip insecure warning
+ // Delete the first entry (of 4), "tempuser1"
+ doKey("down");
+ var numLogins;
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 5, "Correct number of logins before deleting one");
+
+ let countChangedPromise = notifyMenuChanged(4);
+ var deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 4, "Correct number of logins after deleting one");
+ yield countChangedPromise;
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_deletion() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check the new first entry (of 3)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_second() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Delete the second entry (of 3), "testuser3"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 3, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_first_after_deletion2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check the new first entry (of 2)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // test 54
+ // Delete the last entry (of 2), "zzzuser4"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 2, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_3_deletions() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check the only remaining entry
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_check_only_entry_remaining() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // test 56
+ // Delete the only remaining entry, "testuser2"
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 1, "Correct number of logins after deleting one");
+
+ // remove the login that's not shown in the list.
+ setupScript.sendSyncMessage("removeLogin", "login0");
+});
+
+// Tests for single-user forms for ignoring autocomplete=off
+add_task(function* test_form2() {
+ // Turn our attention to form2
+ uname = $_(2, "uname");
+ pword = $_(2, "pword");
+ checkACForm("singleuser5", "singlepass5");
+
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form3() {
+ uname = $_(3, "uname");
+ pword = $_(3, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form4() {
+ uname = $_(4, "uname");
+ pword = $_(4, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form5() {
+ uname = $_(5, "uname");
+ pword = $_(5, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6() {
+ // (this is a control, w/o autocomplete=off, to ensure the login
+ // that was being suppressed would have been filled in otherwise)
+ uname = $_(6, "uname");
+ pword = $_(6, "pword");
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6_changeUsername() {
+ // Test that the password field remains filled in after changing
+ // the username.
+ uname.focus();
+ doKey("right");
+ sendChar("X");
+ // Trigger the 'blur' event on uname
+ pword.focus();
+ yield spinEventLoop();
+ checkACForm("singleuser5X", "singlepass5");
+
+ setupScript.sendSyncMessage("removeLogin", "login5");
+});
+
+add_task(function* test_form7() {
+ uname = $_(7, "uname");
+ pword = $_(7, "pword");
+ checkACForm("", "");
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ var newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ pword.parentNode.insertBefore(newField, pword);
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+
+ // Delete login6B. It was created just to prevent filling in a login
+ // automatically, removing it makes it more likely that we'll catch a
+ // future regression with form filling here.
+ setupScript.sendSyncMessage("removeLogin", "login6B");
+});
+
+add_task(function* test_form7_2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ // The form changes, so we expect the old username field to get the
+ // selected autocomplete value, but neither the new username field nor
+ // the password field should have any values filled in.
+ yield spinEventLoop();
+ checkACForm("form7user1", "");
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+ restoreForm(); // clear field, so reloading test doesn't fail
+
+ setupScript.sendSyncMessage("removeLogin", "login6A");
+});
+
+add_task(function* test_form8() {
+ uname = $_(8, "uname");
+ pword = $_(8, "pword");
+ checkACForm("form8user", "form8pass");
+ restoreForm();
+});
+
+add_task(function* test_form8_blur() {
+ checkACForm("", "");
+ // Focus the previous form to trigger a blur.
+ $_(7, "uname").focus();
+});
+
+add_task(function* test_form8_2() {
+ checkACForm("", "");
+ restoreForm();
+});
+
+add_task(function* test_form8_3() {
+ checkACForm("", "");
+ setupScript.sendSyncMessage("removeLogin", "login7");
+});
+
+add_task(function* test_form9_filtering() {
+ // Turn our attention to form9 to test the dropdown - bug 497541
+ uname = $_(9, "uname");
+ pword = $_(9, "pword");
+ uname.focus();
+ let shownPromise = promiseACShown();
+ sendString("form9userAB");
+ yield shownPromise;
+
+ checkACForm("form9userAB", "");
+ uname.focus();
+ doKey("left");
+ shownPromise = promiseACShown();
+ sendChar("A");
+ let results = yield shownPromise;
+
+ checkACForm("form9userAAB", "");
+ checkArrayValues(results, ["This connection is not secure. Logins entered here could be compromised. Learn More", "form9userAAB"],
+ "Check dropdown is updated after inserting 'A'");
+ doKey("down"); // skip insecure warning
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("form9userAAB", "form9pass");
+});
+
+add_task(function* test_form9_autocomplete_cache() {
+ // Note that this addLogin call will only be seen by the autocomplete
+ // attempt for the sendChar if we do not successfully cache the
+ // autocomplete results.
+ setupScript.sendSyncMessage("addLogin", "login8C");
+ uname.focus();
+ let promise0 = notifyMenuChanged(1);
+ let shownPromise = promiseACShown();
+ sendChar("z");
+ yield promise0;
+ yield shownPromise;
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup should open");
+
+ // check that empty results are cached - bug 496466
+ promise0 = notifyMenuChanged(1);
+ sendChar("z");
+ yield promise0;
+ popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup stays opened due to cached empty result");
+});
+
+add_task(function* test_form11_recipes() {
+ yield loadRecipes({
+ siteRecipes: [{
+ "hosts": ["mochi.test:8888"],
+ "usernameSelector": "input[name='1']",
+ "passwordSelector": "input[name='2']"
+ }],
+ });
+ uname = $_(11, "1");
+ pword = $_(11, "2");
+
+ // First test DOMAutocomplete
+ // Switch the password field to type=password so _fillForm marks the username
+ // field for autocomplete.
+ pword.type = "password";
+ yield promiseFormsProcessed();
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+
+ // Now test recipes with blur on the username field.
+ restoreForm();
+ checkACForm("", "");
+ uname.value = "testuser10";
+ checkACForm("testuser10", "");
+ doKey("tab");
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+ yield resetRecipes();
+});
+
+add_task(function* test_form12_formless() {
+ // Test form-less autocomplete
+ uname = $_(12, "uname");
+ pword = $_(12, "pword");
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Trigger autocomplete
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ let processedPromise = promiseFormsProcessed();
+ doKey("return"); // not "enter"!
+ yield processedPromise;
+ checkACForm("testuser", "testpass");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
new file mode 100644
index 000000000..c3a894958
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login, contextual inscure password warning without saved logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: contextual inscure password warning without saved logins
+
+<script>
+let chromeScript = runChecksAfterCommonInit();
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: contextual inscure password warning without saved logins. **/
+
+// Set to pref before the document loads.
+SpecialPowers.setBoolPref(
+ "security.insecure_field_warning.contextual.enabled", true);
+
+SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref(
+ "security.insecure_field_warning.contextual.enabled");
+});
+
+let uname = $_(1, "uname");
+let pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ let formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form1_warning_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup is opened");
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ doKey("down"); // select insecure warning
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACForm("", "");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html
new file mode 100644
index 000000000..2b6da33ec
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for maxlength attributes</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 391514
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- normal form. -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- limited username -->
+ <form id="form2" action="formtest.js">
+ <input type="text" name="uname" maxlength="4">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited password -->
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="4">
+ </form>
+
+ <!-- limited username and password -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname" maxlength="4">
+ <input type="password" name="pword" maxlength="4">
+ </form>
+
+
+ <!-- limited username -->
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname" maxlength="0">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited password -->
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="0">
+ </form>
+
+ <!-- limited username and password -->
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname" maxlength="0">
+ <input type="password" name="pword" maxlength="0">
+ </form>
+
+
+ <!-- limited, but ok, username -->
+ <form id="form8" action="formtest.js">
+ <input type="text" name="uname" maxlength="999">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited, but ok, password -->
+ <form id="form9" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="999">
+ </form>
+
+ <!-- limited, but ok, username and password -->
+ <form id="form10" action="formtest.js">
+ <input type="text" name="uname" maxlength="999">
+ <input type="password" name="pword" maxlength="999">
+ </form>
+
+
+ <!-- limited, but ok, username -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form11" action="formtest.js">
+ <input type="text" name="uname" maxlength="8">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited, but ok, password -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form12" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="8">
+ </form>
+
+ <!-- limited, but ok, username and password -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form13" action="formtest.js">
+ <input type="text" name="uname" maxlength="8">
+ <input type="password" name="pword" maxlength="8">
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 391514 (Login Manager gets confused with
+ * password/PIN on usaa.com)
+ */
+
+function startTest() {
+ var i;
+
+ is($_(1, "uname").value, "testuser", "Checking for filled username 1");
+ is($_(1, "pword").value, "testpass", "Checking for filled password 1");
+
+ for (i = 2; i < 8; i++) {
+ is($_(i, "uname").value, "", "Checking for unfilled username " + i);
+ is($_(i, "pword").value, "", "Checking for unfilled password " + i);
+ }
+
+ for (i = 8; i < 14; i++) {
+ is($_(i, "uname").value, "testuser", "Checking for filled username " + i);
+ is($_(i, "pword").value, "testpass", "Checking for filled password " + i);
+ }
+
+ // Note that tests 11-13 are limited to exactly the expected value.
+ // Assert this lest someone change the login we're testing with.
+ is($_(11, "uname").value.length, 8, "asserting test assumption is valid.");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
new file mode 100644
index 000000000..443c8a5e9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
@@ -0,0 +1,291 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: multiple login autocomplete
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+ // login0 has no username, so should be filtered out from the autocomplete list.
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "user0pass", "", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword");
+
+ var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword");
+
+ var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword");
+
+
+ // try/catch in case someone runs the tests manually, twice.
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ Services.logins.addLogin(login4);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+
+ addMessageListener("addLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to add is defined: " + loginVariableName);
+ Services.logins.addLogin(login);
+ });
+ addMessageListener("removeLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
+ Services.logins.removeLogin(login);
+ });
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form2" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" readonly="true">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" disabled="true">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function* reinitializeForm(index) {
+ // Using innerHTML is for creating the autocomplete popup again, so the
+ // preference value will be applied to the constructor of
+ // UserAutoCompleteResult.
+ let form = document.getElementById("form" + index);
+ let temp = form.innerHTML;
+ form.innerHTML = "";
+ form.innerHTML = temp;
+
+ yield new Promise(resolve => {
+ let observer = SpecialPowers.wrapCallback(() => {
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ resolve();
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
+ });
+
+ yield SimpleTest.promiseFocus(window);
+
+ uname = $_(index, "uname");
+ pword = $_(index, "pword");
+ uname.value = "";
+ pword.value = "";
+ pword.focus();
+}
+
+function generateDateString(date) {
+ let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+ return dateAndTimeFormatter.format(date);
+}
+
+const DATE_NOW_STRING = generateDateString(new Date());
+
+// Check for expected username/password in form.
+function checkACFormPasswordField(expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACFormPasswordField("");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form2_password_readonly() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(2);
+
+ // Trigger autocomplete popup
+ doKey("down"); // open
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed for a readonly field.");
+});
+
+add_task(function* test_form3_password_disabled() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(3);
+
+ // Trigger autocomplete popup
+ doKey("down"); // open
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed for a disabled field.");
+});
+
+add_task(function* test_form1_enabledInsecureFieldWarning_enabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(1);
+ // Trigger autocomplete popup
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised. Learn More",
+ "No username (" + DATE_NOW_STRING + ")",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select insecure warning
+ checkACFormPasswordField(""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACFormPasswordField("");
+});
+
+add_task(function* test_form1_disabledInsecureFieldWarning_enabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", false],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(1);
+
+ // Trigger autocomplete popup
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["No username (" + DATE_NOW_STRING + ")",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select first item
+ checkACFormPasswordField(""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACFormPasswordField("user0pass");
+});
+
+add_task(function* test_form1_enabledInsecureFieldWarning_disabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", false]
+ ]});
+ yield reinitializeForm(1);
+
+ // Trigger autocomplete popup
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised. Learn More",
+ "No username (" + DATE_NOW_STRING + ")",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select insecure warning
+ checkACFormPasswordField(""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACFormPasswordField("");
+});
+
+add_task(function* test_form1_disabledInsecureFieldWarning_disabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", false],
+ ["signon.autofillForms.http", false]
+ ]});
+ yield reinitializeForm(1);
+
+ // Trigger autocomplete popup
+ doKey("down"); // open
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed with no AutoFillForms.");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html
new file mode 100644
index 000000000..e107cebe6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that passwords only get filled in type=password</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 242956
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- pword is not a type=password input -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="text" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- uname is not a type=text input -->
+ <form id="form2" action="formtest.js">
+ <input type="password" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- two "pword" inputs, (text + password) -->
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="text" name="pword">
+ <input type="password" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- same thing, different order -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="text" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- uname is not a type=text input (try a checkbox just for variety) -->
+ <form id="form5" action="formtest.js">
+ <input type="checkbox" name="uname" value="">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- pword is not a type=password input (try a checkbox just for variety) -->
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="checkbox" name="pword" value="">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- pword is not a type=password input -->
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="text" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 242956 (Stored password is inserted into a
+ readable text input on a second page) **/
+
+// Make sure that pwmgr only puts passwords into type=password <input>s.
+// Might as well test the converse, too (username in password field).
+
+function startTest() {
+ is($_(1, "uname").value, "", "Checking for unfilled username 1");
+ is($_(1, "pword").value, "", "Checking for unfilled password 1");
+
+ is($_(2, "uname").value, "testpass", "Checking for password not username 2");
+ is($_(2, "pword").value, "", "Checking for unfilled password 2");
+
+ is($_(3, "uname").value, "", "Checking for unfilled username 3");
+ is($_(3, "pword").value, "testuser", "Checking for unfilled password 3");
+ is($_(3, "qword").value, "testpass", "Checking for unfilled qassword 3");
+
+ is($_(4, "uname").value, "testuser", "Checking for password not username 4");
+ is($_(4, "pword").value, "testpass", "Checking for unfilled password 4");
+ is($_(4, "qword").value, "", "Checking for unfilled qassword 4");
+
+ is($_(5, "uname").value, "", "Checking for unfilled username 5");
+ is($_(5, "pword").value, "testpass", "Checking for filled password 5");
+
+ is($_(6, "uname").value, "", "Checking for unfilled username 6");
+ is($_(6, "pword").value, "", "Checking for unfilled password 6");
+
+ is($_(7, "uname").value, "testuser", "Checking for unmodified username 7");
+ is($_(7, "pword").value, "", "Checking for unfilled password 7");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
new file mode 100644
index 000000000..1050ab66b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
@@ -0,0 +1,705 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test prompter.{prompt,promptPassword,promptUsernameAndPassword}</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var state, action;
+var uname = { value: null };
+var pword = { value: null };
+var result = { value: null };
+var isOk;
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+let prompterParent = runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let prompter1 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt);
+
+ addMessageListener("proxyPrompter", function onMessage(msg) {
+ let rv = prompter1[msg.methodName](...msg.args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+});
+
+let prompter1 = new PrompterProxy(prompterParent);
+
+const defaultTitle = "the title";
+const defaultMsg = "the message";
+
+function initLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ var login1, login2A, login2B, login2C, login2D, login2E;
+ var pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2D = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2E = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://example.com", null, "http://example.com",
+ "", "examplepass", "", "");
+ login2A.init("http://example2.com", null, "http://example2.com",
+ "user1name", "user1pass", "", "");
+ login2B.init("http://example2.com", null, "http://example2.com",
+ "user2name", "user2pass", "", "");
+ login2C.init("http://example2.com", null, "http://example2.com",
+ "user3.name@host", "user3pass", "", "");
+ login2D.init("http://example2.com", null, "http://example2.com",
+ "100@beef", "user3pass", "", "");
+ login2E.init("http://example2.com", null, "http://example2.com",
+ "100%beef", "user3pass", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+ pwmgr.addLogin(login2C);
+ pwmgr.addLogin(login2D);
+ pwmgr.addLogin(login2E);
+}
+
+add_task(function* setup() {
+ runInParent(initLogins);
+});
+
+add_task(function* test_prompt_accept() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "abc",
+ passValue : "",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "xyz",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(result.value, "xyz", "Checking prompt() returned value");
+});
+
+add_task(function* test_prompt_cancel() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "abc",
+ passValue : "",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result);
+ yield promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_promptPassword_defaultAccept() {
+ // Default password provided, existing logins are ignored.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "inputpw",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "secret",
+ };
+ pword.value = "inputpw";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_defaultCancel() {
+ // Default password provided, existing logins are ignored.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "inputpw",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ pword.value = "inputpw";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_promptPassword_emptyAccept() {
+ // No default password provided, realm does not match existing login.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "secret",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_saved() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "examplepass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_noMatchingPasswordForEmptyUN() {
+ // No default password provided, none of the logins from this host are
+ // password-only so the user is prompted.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "secret",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_matchingPWForUN() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://user1name@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user1pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_matchingPWForUN2() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://user2name@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_matchingPWForUN3() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://user3%2Ename%40host@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_extraAt() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://100@beef@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_usernameEncoding() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://100%25beef@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+
+ // XXX test saving a password with Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY
+});
+
+add_task(function* test_promptPassword_realm() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "fill2pass",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_realm2() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "fill2pass",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_accept() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "inuser",
+ passValue : "inpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "outuser",
+ passField : "outpass",
+ };
+ uname.value = "inuser";
+ pword.value = "inpass";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "outuser", "Checking returned username");
+ is(pword.value, "outpass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_cancel() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "inuser",
+ passValue : "inpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ uname.value = "inuser";
+ pword.value = "inpass";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ yield promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_promptUsernameAndPassword_autofill() {
+ // test filling in existing password-only login
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "examplepass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "", "Checking returned username");
+ is(pword.value, "examplepass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_multipleExisting() {
+ // test filling in existing login (undetermined from multiple selection)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ ok(uname.value == "user1name" || uname.value == "user2name", "Checking returned username");
+ ok(pword.value == "user1pass" || uname.value == "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_multipleExisting1() {
+ // test filling in existing login (user1 from multiple selection)
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = "user1name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user1name", "Checking returned username");
+ is(pword.value, "user1pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_multipleExisting2() {
+ // test filling in existing login (user2 from multiple selection)
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user2name",
+ passValue : "user2pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_passwordChange() {
+ // test changing password
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user2name",
+ passValue : "user2pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "NEWuser2pass",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "NEWuser2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_changePasswordBack() {
+ // test changing password (back to original value)
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user2name",
+ passValue : "NEWuser2pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "user2pass",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_realm() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "fill2user",
+ passField : "fill2pass",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "fill2user", "Checking returned username");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_realm2() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "fill2user",
+ passField : "fill2pass",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "fill2user", "Checking returned username");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
new file mode 100644
index 000000000..0dc8fdf9c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
@@ -0,0 +1,362 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test HTTP auth prompts by loading authenticate.sjs</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var iframe = document.getElementById("iframe");
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+const AUTHENTICATE_PATH = new URL("authenticate.sjs", window.location.href).pathname;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ let login3A, login3B, login4;
+ login3A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login3B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let httpUpgradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let httpsDowngradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let dedupeHttpUpgradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let dedupeHttpsUpgradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+
+ login3A.init("http://mochi.test:8888", null, "mochitest",
+ "mochiuser1", "mochipass1", "", "");
+ login3B.init("http://mochi.test:8888", null, "mochitest2",
+ "mochiuser2", "mochipass2", "", "");
+ login4.init("http://mochi.test:8888", null, "mochitest3",
+ "mochiuser3", "mochipass3-old", "", "");
+ // Logins to test scheme upgrades (allowed) and downgrades (disallowed)
+ httpUpgradeLogin.init("http://example.com", null, "schemeUpgrade",
+ "httpUser", "httpPass", "", "");
+ httpsDowngradeLogin.init("https://example.com", null, "schemeDowngrade",
+ "httpsUser", "httpsPass", "", "");
+ // HTTP and HTTPS version of the same domain and realm but with different passwords.
+ dedupeHttpUpgradeLogin.init("http://example.org", null, "schemeUpgradeDedupe",
+ "dedupeUser", "httpPass", "", "");
+ dedupeHttpsUpgradeLogin.init("https://example.org", null, "schemeUpgradeDedupe",
+ "dedupeUser", "httpsPass", "", "");
+
+
+ pwmgr.addLogin(login3A);
+ pwmgr.addLogin(login3B);
+ pwmgr.addLogin(login4);
+ pwmgr.addLogin(httpUpgradeLogin);
+ pwmgr.addLogin(httpsDowngradeLogin);
+ pwmgr.addLogin(dedupeHttpUpgradeLogin);
+ pwmgr.addLogin(dedupeHttpsUpgradeLogin);
+});
+
+add_task(function* test_iframe() {
+ let state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest”",
+ title : "Authentication Required",
+ textValue : "mochiuser1",
+ passValue : "mochipass1",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ var iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe.contentDocument);
+
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest2”",
+ title : "Authentication Required",
+ textValue : "mochiuser2",
+ passValue : "mochipass2",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ promptDone = handlePrompt(state, action);
+ // We've already authenticated to this host:port. For this next
+ // request, the existing auth should be sent, we'll get a 401 reply,
+ // and we should prompt for new auth.
+ iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest2";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
+ iframe.contentDocument);
+
+ // Now make a load that requests the realm from test 1000. It was
+ // already provided there, so auth will *not* be prompted for -- the
+ // networking layer already knows it!
+ iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe.contentDocument);
+
+ // Same realm we've already authenticated to, but with a different
+ // expected password (to trigger an auth prompt, and change-password
+ // popup notification).
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest”",
+ title : "Authentication Required",
+ textValue : "mochiuser1",
+ passValue : "mochipass1",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "mochipass1-new",
+ };
+ promptDone = handlePrompt(state, action);
+ iframeLoaded = onloadPromiseFor("iframe");
+ let promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1-new";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1-new"},
+ iframe.contentDocument);
+ yield promptShownPromise;
+
+ // Same as last test, but for a realm we haven't already authenticated
+ // to (but have an existing saved login for, so that we'll trigger
+ // a change-password popup notification.
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest3”",
+ title : "Authentication Required",
+ textValue : "mochiuser3",
+ passValue : "mochipass3-old",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "mochipass3-new",
+ };
+ promptDone = handlePrompt(state, action);
+ iframeLoaded = onloadPromiseFor("iframe");
+ promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
+ iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-new&realm=mochitest3";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-new"},
+ iframe.contentDocument);
+ yield promptShownPromise;
+
+ // Housekeeping: Delete login4 to test the save prompt in the next test.
+ runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ var tmpLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ tmpLogin.init("http://mochi.test:8888", null, "mochitest3",
+ "mochiuser3", "mochipass3-old", "", "");
+ Services.logins.removeLogin(tmpLogin);
+
+ // Clear cached auth from this subtest, and avoid leaking due to bug 459620.
+ var authMgr = Cc['@mozilla.org/network/http-auth-manager;1'].
+ getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+ });
+
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest3”",
+ title : "Authentication Required",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "mochiuser3",
+ passField : "mochipass3-old",
+ };
+ // Trigger a new prompt, so we can test adding a new login.
+ promptDone = handlePrompt(state, action);
+
+ iframeLoaded = onloadPromiseFor("iframe");
+ promptShownPromise = promisePromptShown("passwordmgr-prompt-save");
+ iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-old&realm=mochitest3";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-old"},
+ iframe.contentDocument);
+ yield promptShownPromise;
+});
+
+add_task(function* test_schemeUpgrade() {
+ let state = {
+ msg : "https://example.com is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!",
+ title : "Authentication Required",
+ textValue : "httpUser",
+ passValue : "httpPass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ let iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "https://example.com" + AUTHENTICATE_PATH +
+ "?user=httpUser&pass=httpPass&realm=schemeUpgrade";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "httpUser", pass: "httpPass"},
+ SpecialPowers.wrap(iframe).contentDocument);
+});
+
+add_task(function* test_schemeDowngrade() {
+ let state = {
+ msg : "http://example.com is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!",
+ title : "Authentication Required",
+ textValue : "", // empty because we shouldn't downgrade
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "cancel",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ let iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "http://example.com" + AUTHENTICATE_PATH +
+ "?user=unused&pass=unused&realm=schemeDowngrade";
+ yield promptDone;
+ yield iframeLoaded;
+});
+
+add_task(function* test_schemeUpgrade_dedupe() {
+ let state = {
+ msg : "https://example.org is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!",
+ title : "Authentication Required",
+ textValue : "dedupeUser",
+ passValue : "httpsPass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ let iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "https://example.org" + AUTHENTICATE_PATH +
+ "?user=dedupeUser&pass=httpsPass&realm=schemeUpgradeDedupe";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "dedupeUser", pass: "httpsPass"},
+ SpecialPowers.wrap(iframe).contentDocument);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
new file mode 100644
index 000000000..92af172ca
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test HTTP auth prompts by loading authenticate.sjs with no window</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login.init("http://mochi.test:8888", null, "mochitest",
+ "mochiuser1", "mochipass1", "", "");
+ Services.logins.addLogin(login);
+});
+
+add_task(function* test_sandbox_xhr() {
+ let state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest”",
+ title : "Authentication Required",
+ textValue : "mochiuser1",
+ passValue : "mochipass1",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ let url = new URL("authenticate.sjs?user=mochiuser1&pass=mochipass1", window.location.href);
+ let sandboxConstructor = SpecialPowers.Cu.Sandbox;
+ let sandbox = new sandboxConstructor(this, {wantXrays: true});
+ function sandboxedRequest(sandboxedUrl) {
+ let req = SpecialPowers.Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(SpecialPowers.Ci.nsIXMLHttpRequest);
+ req.open("GET", sandboxedUrl, true);
+ req.send(null);
+ }
+
+ let loginModifiedPromise = promiseStorageChanged(["modifyLogin"]);
+ sandbox.sandboxedRequest = sandboxedRequest(url);
+ info("send the XHR request in the sandbox");
+ SpecialPowers.Cu.evalInSandbox("sandboxedRequest;", sandbox);
+
+ yield promptDone;
+ info("prompt shown, waiting for metadata updates");
+ // Ensure the timeLastUsed and timesUsed metadata are updated.
+ yield loginModifiedPromise;
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
new file mode 100644
index 000000000..36f53a54a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
@@ -0,0 +1,406 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test promptAuth prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var state, action;
+var isOk;
+
+var level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+var authinfo = {
+ username : "",
+ password : "",
+ domain : "",
+
+ flags : Ci.nsIAuthInformation.AUTH_HOST,
+ authenticationScheme : "basic",
+ realm : ""
+};
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let prompterParent = runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let prompter2 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt2);
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ let channels = {};
+ channels.channel1 = ioService.newChannel2("http://example.com",
+ null,
+ null,
+ null, // aLoadingNode
+ Services.
+ scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ channels.channel2 = ioService.newChannel2("http://example2.com",
+ null,
+ null,
+ null, // aLoadingNode
+ Services.
+ scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ addMessageListener("proxyPrompter", function onMessage(msg) {
+ let args = [...msg.args];
+ let channelName = args.shift();
+ // Replace the channel name string (arg. 0) with the channel by that name.
+ args.unshift(channels[channelName]);
+
+ let rv = prompter2[msg.methodName](...args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ let login1, login2A, login2B, login2C, login2D, login2E;
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2D = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2E = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://example.com", null, "http://example.com",
+ "", "examplepass", "", "");
+ login2A.init("http://example2.com", null, "http://example2.com",
+ "user1name", "user1pass", "", "");
+ login2B.init("http://example2.com", null, "http://example2.com",
+ "user2name", "user2pass", "", "");
+ login2C.init("http://example2.com", null, "http://example2.com",
+ "user3.name@host", "user3pass", "", "");
+ login2D.init("http://example2.com", null, "http://example2.com",
+ "100@beef", "user3pass", "", "");
+ login2E.init("http://example2.com", null, "http://example2.com",
+ "100%beef", "user3pass", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+ pwmgr.addLogin(login2C);
+ pwmgr.addLogin(login2D);
+ pwmgr.addLogin(login2E);
+});
+
+let prompter2 = new PrompterProxy(prompterParent);
+
+add_task(function* test_accept() {
+ state = {
+ msg : "http://example.com is requesting your username and password. The site says: “some realm”",
+ title : "Authentication Required",
+ textValue : "inuser",
+ passValue : "inpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "outuser",
+ passField : "outpass",
+ };
+ authinfo.username = "inuser";
+ authinfo.password = "inpass";
+ authinfo.realm = "some realm";
+
+ promptDone = handlePrompt(state, action);
+ // Since prompter2 is actually a proxy to send a message to a chrome script and
+ // we can't send a channel in a message, we instead send the channel name that
+ // already exists in the chromeScript.
+ isOk = prompter2.promptAuth("channel1", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "outuser", "Checking returned username");
+ is(authinfo.password, "outpass", "Checking returned password");
+});
+
+add_task(function* test_cancel() {
+ state = {
+ msg : "http://example.com is requesting your username and password. The site says: “some realm”",
+ title : "Authentication Required",
+ textValue : "outuser",
+ passValue : "outpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel1", level, authinfo);
+ yield promptDone;
+
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_pwonly() {
+ // test filling in password-only login
+ state = {
+ msg : "http://example.com is requesting your username and password. The site says: “http://example.com”",
+ title : "Authentication Required",
+ textValue : "",
+ passValue : "examplepass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel1", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "", "Checking returned username");
+ is(authinfo.password, "examplepass", "Checking returned password");
+});
+
+add_task(function* test_multipleExisting() {
+ // test filling in existing login (undetermined from multiple selection)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ ok(authinfo.username == "user1name" || authinfo.username == "user2name", "Checking returned username");
+ ok(authinfo.password == "user1pass" || authinfo.password == "user2pass", "Checking returned password");
+});
+
+add_task(function* test_multipleExisting2() {
+ // test filling in existing login (undetermined --> user1)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // enter one of the known logins, test 504+505 exercise the two possible states.
+ action = {
+ buttonClick : "ok",
+ textField : "user1name",
+ passField : "user1pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user1name", "Checking returned username");
+ is(authinfo.password, "user1pass", "Checking returned password");
+});
+
+add_task(function* test_multipleExisting3() {
+ // test filling in existing login (undetermined --> user2)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // enter one of the known logins, test 504+505 exercise the two possible states.
+ action = {
+ buttonClick : "ok",
+ textField : "user2name",
+ passField : "user2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_changingMultiple() {
+ // test changing a password (undetermined --> user2 w/ newpass)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // force to user2, and change the password
+ action = {
+ buttonClick : "ok",
+ textField : "user2name",
+ passField : "NEWuser2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "NEWuser2pass", "Checking returned password");
+});
+
+add_task(function* test_changingMultiple2() {
+ // test changing a password (undetermined --> user2 w/ origpass)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // force to user2, and change the password back
+ action = {
+ buttonClick : "ok",
+ textField : "user2name",
+ passField : "user2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "user2pass", "Checking returned password");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
new file mode 100644
index 000000000..95dd4c7bc
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
@@ -0,0 +1,264 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test promptAuth proxy prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var state, action;
+var pwmgr;
+var proxyLogin;
+var isOk;
+var mozproxy, proxiedHost = "http://mochi.test:8888";
+var proxyChannel;
+var systemPrincipal = SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal();
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+var level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+
+var proxyAuthinfo = {
+ username : "",
+ password : "",
+ domain : "",
+
+ flags : Ci.nsIAuthInformation.AUTH_PROXY,
+ authenticationScheme : "basic",
+ realm : ""
+};
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+const Cc_promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"];
+ok(Cc_promptFac != null, "Access Cc[@mozilla.org/passwordmanager/authpromptfactory;1]");
+
+const Ci_promptFac = Ci.nsIPromptFactory;
+ok(Ci_promptFac != null, "Access Ci.nsIPromptFactory");
+
+const promptFac = Cc_promptFac.getService(Ci_promptFac);
+ok(promptFac != null, "promptFac getService()");
+
+var prompter2 = promptFac.getPrompt(window, Ci.nsIAuthPrompt2);
+
+function initLogins(pi) {
+ pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ mozproxy = "moz-proxy://" + SpecialPowers.wrap(pi).host + ":" +
+ SpecialPowers.wrap(pi).port;
+
+ proxyLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ proxyLogin.init(mozproxy, null, "Proxy Realm",
+ "proxuser", "proxpass", "", "");
+
+ pwmgr.addLogin(proxyLogin);
+}
+
+var startupCompleteResolver;
+var startupComplete = new Promise(resolve => startupCompleteResolver = resolve);
+
+function proxyChannelListener() { }
+proxyChannelListener.prototype = {
+ onStartRequest: function(request, context) {
+ startupCompleteResolver();
+ },
+ onStopRequest: function(request, context, status) { }
+};
+
+var resolveCallback = SpecialPowers.wrapCallbackObject({
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ onProxyAvailable : function (req, uri, pi, status) {
+ initLogins(pi);
+
+ // I'm cheating a bit here... We should probably do some magic foo to get
+ // something implementing nsIProxiedProtocolHandler and then call
+ // NewProxiedChannel(), so we have something that's definately a proxied
+ // channel. But Mochitests use a proxy for a number of hosts, so just
+ // requesting a normal channel will give us a channel that's proxied.
+ // The proxyChannel needs to move to at least on-modify-request to
+ // have valid ProxyInfo, but we use OnStartRequest during startup()
+ // for simplicity.
+ proxyChannel = ioService.newChannel2(proxiedHost,
+ null,
+ null,
+ null, // aLoadingNode
+ systemPrincipal,
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ proxyChannel.asyncOpen2(SpecialPowers.wrapCallbackObject(new proxyChannelListener()));
+ }
+});
+
+function startup() {
+ // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy.
+ var ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"].
+ getService(SpecialPowers.Ci.nsIIOService);
+
+ var pps = SpecialPowers.Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+ var channel = ios.newChannel2("http://example.com",
+ null,
+ null,
+ null, // aLoadingNode
+ systemPrincipal,
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ pps.asyncResolve(channel, 0, resolveCallback);
+}
+
+startup();
+
+add_task(function* setup() {
+ info("Waiting for startup to complete...");
+ yield startupComplete;
+});
+
+add_task(function* test_noAutologin() {
+ // test proxy login (default = no autologin), make sure it prompts.
+ state = {
+ msg : "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”",
+ title : "Authentication Required",
+ textValue : "proxuser",
+ passValue : "proxpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ var time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
+ yield promptDone;
+ var time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(function* test_autologin() {
+ // test proxy login (with autologin)
+
+ // Enable the autologin pref.
+ prefs.setBoolPref("signon.autologin.proxy", true);
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
+ time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(function* test_autologin_incorrect() {
+ // test proxy login (with autologin), ensure it prompts after a failed auth.
+ state = {
+ msg : "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”",
+ title : "Authentication Required",
+ textValue : "proxuser",
+ passValue : "proxpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = (Ci.nsIAuthInformation.AUTH_PROXY | Ci.nsIAuthInformation.PREVIOUS_FAILED);
+
+ time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
+ yield promptDone;
+ time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(function* test_autologin_private() {
+ // test proxy login (with autologin), ensure it prompts in Private Browsing mode.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "proxuser",
+ passValue : "proxpass",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ prefs.clearUserPref("signon.autologin.proxy");
+
+ // XXX check for and kill popup notification??
+ // XXX check for checkbox / checkstate on old prompts?
+ // XXX check NTLM domain stuff
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html
new file mode 100644
index 000000000..943bffc52
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for recipes overriding login fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+let fillPromiseResolvers = [];
+
+function waitForFills(fillCount) {
+ let promises = [];
+ while (fillCount--) {
+ let promise = new Promise(resolve => fillPromiseResolvers.push(resolve));
+ promises.push(promise);
+ }
+
+ return Promise.all(promises);
+}
+
+add_task(function* setup() {
+ if (document.readyState !== "complete") {
+ yield new Promise((resolve) => {
+ document.onreadystatechange = () => {
+ if (document.readyState !== "complete") {
+ return;
+ }
+ document.onreadystatechange = null;
+ resolve();
+ };
+ });
+ }
+
+ document.getElementById("content")
+ .addEventListener("input", function handleInputEvent(evt) {
+ let resolve = fillPromiseResolvers.shift();
+ if (!resolve) {
+ ok(false, "Too many fills");
+ return;
+ }
+
+ resolve(evt.target);
+ });
+});
+
+add_task(function* loadUsernamePasswordSelectorRecipes() {
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='uname1']",
+ passwordSelector: "input[name='pword2']",
+ }],
+ });
+});
+
+add_task(function* testOverriddingFields() {
+ // Insert the form dynamically so autofill is triggered after setup above.
+ document.getElementById("content").innerHTML = `
+ <!-- form with recipe for the username and password -->
+ <form id="form1">
+ <input type="text" name="uname1" data-expected="true">
+ <input type="text" name="uname2" data-expected="false">
+ <input type="password" name="pword1" data-expected="false">
+ <input type="password" name="pword2" data-expected="true">
+ </form>`;
+
+ let elements = yield waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(function* testDefaultHeuristics() {
+ // Insert the form dynamically so autofill is triggered after setup above.
+ document.getElementById("content").innerHTML = `
+ <!-- Fallback to the default heuristics since the selectors don't match -->
+ <form id="form2">
+ <input type="text" name="uname3" data-expected="false">
+ <input type="text" name="uname4" data-expected="true">
+ <input type="password" name="pword3" data-expected="true">
+ <input type="password" name="pword4" data-expected="false">
+ </form>`;
+
+ let elements = yield waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(function* loadNotUsernameSelectorRecipes() {
+ yield resetRecipes();
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ notUsernameSelector: "input[name='not_uname1']"
+ }],
+ });
+});
+
+add_task(function* testNotUsernameField() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notUsernameSelector should be skipped -->
+ <form id="form3">
+ <input type="text" name="uname5" data-expected="true">
+ <input type="text" name="not_uname1" data-expected="false">
+ <input type="password" name="pword5" data-expected="true">
+ </form>`;
+
+ let elements = yield waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(function* testNotUsernameFieldNoUsername() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notUsernameSelector should be skipped.
+ No username field should be found and filled in this case -->
+ <form id="form4">
+ <input type="text" name="not_uname1" data-expected="false">
+ <input type="password" name="pword6" data-expected="true">
+ </form>`;
+
+ let elements = yield waitForFills(1);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ // Forms are inserted dynamically
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
new file mode 100644
index 000000000..c93c1e9c9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
@@ -0,0 +1,263 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test interaction between autocomplete and focus on username fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let readyPromise = registerRunTests();
+let chromeScript = runInParent(function chromeSetup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+
+ let login1A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login1B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1A.init("http://mochi.test:8888", "http://username-focus-1", null,
+ "testuser1A", "testpass1A", "", "");
+
+ login2A.init("http://mochi.test:8888", "http://username-focus-2", null,
+ "testuser2A", "testpass2A", "", "");
+ login2B.init("http://mochi.test:8888", "http://username-focus-2", null,
+ "testuser2B", "testpass2B", "", "");
+
+ pwmgr.addLogin(login1A);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+});
+</script>
+
+<p id="display"></p>
+<div id="content">
+ <!-- first 3 forms have a matching user+pass login -->
+
+ <!-- user+pass form. -->
+ <form id="form-autofilled" action="http://username-focus-1">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit" name="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form-autofilled-prefilled-un" action="http://username-focus-1">
+ <input type="text" name="uname" value="testuser1A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form. -->
+ <form id="form-autofilled-focused-dynamic" action="http://username-focus-1">
+ <input type="text" name="uname">
+ <input type="not-yet-password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+
+ <!-- next 5 forms have matching user+pass (2x) logins -->
+
+ <!-- user+pass form. -->
+ <form id="form-multiple" action="http://username-focus-2">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form dynamic with existing focus -->
+ <form id="form-multiple-dynamic" action="http://username-focus-2">
+ <input type="text" name="uname">
+ <input type="not-yet-password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form-multiple-prefilled-un1" action="http://username-focus-2">
+ <input type="text" name="uname" value="testuser2A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, different username prefilled -->
+ <form id="form-multiple-prefilled-un2" action="http://username-focus-2">
+ <input type="text" name="uname" value="testuser2B">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled with existing focus -->
+ <form id="form-multiple-prefilled-focused-dynamic" action="http://username-focus-2">
+ <input type="text" name="uname" value="testuser2B">
+ <input type="not-yet-password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function removeFocus() {
+ $_("-autofilled", "submit").focus();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", false],
+ ]});
+
+ ok(readyPromise, "check promise is available");
+ yield readyPromise;
+});
+
+add_task(function* test_autofilled() {
+ let usernameField = $_("-autofilled", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_autofilled_prefilled_un() {
+ let usernameField = $_("-autofilled-prefilled-un", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_autofilled_focused_dynamic() {
+ let usernameField = $_("-autofilled-focused-dynamic", "uname");
+ let passwordField = $_("-autofilled-focused-dynamic", "pword");
+ info("Username and password will be filled while username focused");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+ info("triggering autofill");
+ noPopupPromise = promiseNoUnexpectedPopupShown();
+ passwordField.type = "password";
+ yield noPopupPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed");
+
+ removeFocus();
+ passwordField.value = "test";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+// Begin testing forms that have multiple saved logins
+
+add_task(function* test_multiple() {
+ let usernameField = $_("-multiple", "uname");
+ info("Fields not filled due to multiple so autocomplete upon focus");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_dynamic() {
+ let usernameField = $_("-multiple-dynamic", "uname");
+ let passwordField = $_("-multiple-dynamic", "pword");
+ info("Fields not filled but username is focused upon marking so open");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ info("triggering _fillForm code");
+ let shownPromise = promiseACShown();
+ passwordField.type = "password";
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_prefilled_un1() {
+ let usernameField = $_("-multiple-prefilled-un1", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_prefilled_un2() {
+ let usernameField = $_("-multiple-prefilled-un2", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_prefilled_focused_dynamic() {
+ let usernameField = $_("-multiple-prefilled-focused-dynamic", "uname");
+ let passwordField = $_("-multiple-prefilled-focused-dynamic", "pword");
+ info("Username and password will be filled while username focused");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+ info("triggering autofill");
+ noPopupPromise = promiseNoUnexpectedPopupShown();
+ passwordField.type = "password";
+ yield noPopupPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed");
+
+ removeFocus();
+ passwordField.value = "test";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* cleanup() {
+ removeFocus();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html
new file mode 100644
index 000000000..fa8357792
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=654348
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test XHR auth with user and pass arguments</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="startTest()">
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test checks we correctly ignore authentication entry
+ * for a subpath and use creds from the URL when provided when XHR
+ * is used with filled user name and password.
+ *
+ * 1. connect authenticate.sjs that excepts user1:pass1 password
+ * 2. connect authenticate.sjs that this time expects differentuser2:pass2 password
+ * we must use the creds that are provided to the xhr witch are different and expected
+ */
+
+function doxhr(URL, user, pass, code, next) {
+ var xhr = new XMLHttpRequest();
+ if (user && pass)
+ xhr.open("POST", URL, true, user, pass);
+ else
+ xhr.open("POST", URL, true);
+ xhr.onload = function() {
+ is(xhr.status, code, "expected response code " + code);
+ next();
+ };
+ xhr.onerror = function() {
+ ok(false, "request passed");
+ finishTest();
+ };
+ xhr.send();
+}
+
+function startTest() {
+ doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "dummy", 403, function() {
+ doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "pass1", 200, finishTest);
+ });
+}
+
+function finishTest() {
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/prompt_common.js b/toolkit/components/passwordmgr/test/prompt_common.js
new file mode 100644
index 000000000..267e697ae
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/prompt_common.js
@@ -0,0 +1,79 @@
+/**
+ * NOTE:
+ * This file is currently only being used for tests which haven't been
+ * fixed to work with e10s. Favor using the `prompt_common.js` file that
+ * is in `toolkit/components/prompts/test/` instead.
+ */
+
+var Ci = SpecialPowers.Ci;
+ok(Ci != null, "Access Ci");
+var Cc = SpecialPowers.Cc;
+ok(Cc != null, "Access Cc");
+
+var didDialog;
+
+var timer; // keep in outer scope so it's not GC'd before firing
+function startCallbackTimer() {
+ didDialog = false;
+
+ // Delay before the callback twiddles the prompt.
+ const dialogDelay = 10;
+
+ // Use a timer to invoke a callback to twiddle the authentication dialog
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(observer, dialogDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+
+var observer = SpecialPowers.wrapCallbackObject({
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIObserver,
+ Ci.nsISupports, Ci.nsISupportsWeakReference];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ observe : function (subject, topic, data) {
+ var doc = getDialogDoc();
+ if (doc)
+ handleDialog(doc, testNum);
+ else
+ startCallbackTimer(); // try again in a bit
+ }
+});
+
+function getDialogDoc() {
+ // Find the <browser> which contains notifyWindow, by looking
+ // through all the open windows and all the <browsers> in each.
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ // var enumerator = wm.getEnumerator("navigator:browser");
+ var enumerator = wm.getXULWindowEnumerator(null);
+
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+ var containedDocShells = windowDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeChrome,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ // Get the corresponding document for this docshell
+ var childDocShell = containedDocShells.getNext();
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
+ continue;
+ var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+ .contentViewer
+ .DOMDocument;
+
+ // ok(true, "Got window: " + childDoc.location.href);
+ if (childDoc.location.href == "chrome://global/content/commonDialog.xul")
+ return childDoc;
+ }
+ }
+
+ return null;
+}
diff --git a/toolkit/components/passwordmgr/test/pwmgr_common.js b/toolkit/components/passwordmgr/test/pwmgr_common.js
new file mode 100644
index 000000000..fa7c4fd85
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/pwmgr_common.js
@@ -0,0 +1,509 @@
+const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
+
+/**
+ * Returns the element with the specified |name| attribute.
+ */
+function $_(formNum, name) {
+ var form = document.getElementById("form" + formNum);
+ if (!form) {
+ logWarning("$_ couldn't find requested form " + formNum);
+ return null;
+ }
+
+ var element = form.children.namedItem(name);
+ if (!element) {
+ logWarning("$_ couldn't find requested element " + name);
+ return null;
+ }
+
+ // Note that namedItem is a bit stupid, and will prefer an
+ // |id| attribute over a |name| attribute when looking for
+ // the element. Login Mananger happens to use .namedItem
+ // anyway, but let's rigorously check it here anyway so
+ // that we don't end up with tests that mistakenly pass.
+
+ if (element.getAttribute("name") != name) {
+ logWarning("$_ got confused.");
+ return null;
+ }
+
+ return element;
+}
+
+/**
+ * Check a form for expected values. If an argument is null, a field's
+ * expected value will be the default value.
+ *
+ * <form id="form#">
+ * checkForm(#, "foo");
+ */
+function checkForm(formNum, val1, val2, val3) {
+ var e, form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ var numToCheck = arguments.length - 1;
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[0];
+ if (val1 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val1, "Test value of field " + e.name +
+ " in form " + formNum);
+
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[1];
+ if (val2 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val2, "Test value of field " + e.name +
+ " in form " + formNum);
+
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[2];
+ if (val3 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val3, "Test value of field " + e.name +
+ " in form " + formNum);
+}
+
+/**
+ * Check a form for unmodified values from when page was loaded.
+ *
+ * <form id="form#">
+ * checkUnmodifiedForm(#);
+ */
+function checkUnmodifiedForm(formNum) {
+ var form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ for (var i = 0; i < form.elements.length; i++) {
+ var ele = form.elements[i];
+
+ // No point in checking form submit/reset buttons.
+ if (ele.type == "submit" || ele.type == "reset")
+ continue;
+
+ is(ele.value, ele.defaultValue, "Test to default value of field " +
+ ele.name + " in form " + formNum);
+ }
+}
+
+/**
+ * Mochitest gives us a sendKey(), but it's targeted to a specific element.
+ * This basically sends an untargeted key event, to whatever's focused.
+ */
+function doKey(aKey, modifier) {
+ var keyName = "DOM_VK_" + aKey.toUpperCase();
+ var key = KeyEvent[keyName];
+
+ // undefined --> null
+ if (!modifier)
+ modifier = null;
+
+ // Window utils for sending fake sey events.
+ var wutils = SpecialPowers.wrap(window).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
+
+ if (wutils.sendKeyEvent("keydown", key, 0, modifier)) {
+ wutils.sendKeyEvent("keypress", key, 0, modifier);
+ }
+ wutils.sendKeyEvent("keyup", key, 0, modifier);
+}
+
+/**
+ * Init with a common login
+ * If selfFilling is true or non-undefined, fires an event at the page so that
+ * the test can start checking filled-in values. Tests that check observer
+ * notifications might be confused by this.
+ */
+function commonInit(selfFilling) {
+ var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"].
+ getService(SpecialPowers.Ci.nsILoginManager);
+ ok(pwmgr != null, "Access LoginManager");
+
+ // Check that initial state has no logins
+ var logins = pwmgr.getAllLogins();
+ is(logins.length, 0, "Not expecting logins to be present");
+ var disabledHosts = pwmgr.getAllDisabledHosts();
+ if (disabledHosts.length) {
+ ok(false, "Warning: wasn't expecting disabled hosts to be present.");
+ for (var host of disabledHosts)
+ pwmgr.setLoginSavingEnabled(host, true);
+ }
+
+ // Add a login that's used in multiple tests
+ var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(SpecialPowers.Ci.nsILoginInfo);
+ login.init("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "testuser", "testpass", "uname", "pword");
+ pwmgr.addLogin(login);
+
+ // Last sanity check
+ logins = pwmgr.getAllLogins();
+ is(logins.length, 1, "Checking for successful init login");
+ disabledHosts = pwmgr.getAllDisabledHosts();
+ is(disabledHosts.length, 0, "Checking for no disabled hosts");
+
+ if (selfFilling)
+ return;
+
+ if (this.sendAsyncMessage) {
+ sendAsyncMessage("registerRunTests");
+ } else {
+ registerRunTests();
+ }
+}
+
+function registerRunTests() {
+ return new Promise(resolve => {
+ // We provide a general mechanism for our tests to know when they can
+ // safely run: we add a final form that we know will be filled in, wait
+ // for the login manager to tell us that it's filled in and then continue
+ // with the rest of the tests.
+ window.addEventListener("DOMContentLoaded", (event) => {
+ var form = document.createElement('form');
+ form.id = 'observerforcer';
+ var username = document.createElement('input');
+ username.name = 'testuser';
+ form.appendChild(username);
+ var password = document.createElement('input');
+ password.name = 'testpass';
+ password.type = 'password';
+ form.appendChild(password);
+
+ var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
+ var formLikeRoot = subject.QueryInterface(SpecialPowers.Ci.nsIDOMNode);
+ if (formLikeRoot.id !== 'observerforcer')
+ return;
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ formLikeRoot.remove();
+ SimpleTest.executeSoon(() => {
+ var runTestEvent = new Event("runTests");
+ window.dispatchEvent(runTestEvent);
+ resolve();
+ });
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
+
+ document.body.appendChild(form);
+ });
+ });
+}
+
+const masterPassword = "omgsecret!";
+
+function enableMasterPassword() {
+ setMasterPassword(true);
+}
+
+function disableMasterPassword() {
+ setMasterPassword(false);
+}
+
+function setMasterPassword(enable) {
+ var oldPW, newPW;
+ if (enable) {
+ oldPW = "";
+ newPW = masterPassword;
+ } else {
+ oldPW = masterPassword;
+ newPW = "";
+ }
+ // Set master password. Note that this does not log you in, so the next
+ // invocation of pwmgr can trigger a MP prompt.
+
+ var pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB);
+ var token = pk11db.findTokenByName("");
+ info("MP change from " + oldPW + " to " + newPW);
+ token.changePassword(oldPW, newPW);
+}
+
+function logoutMasterPassword() {
+ var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+}
+
+function dumpLogins(pwmgr) {
+ var logins = pwmgr.getAllLogins();
+ ok(true, "----- dumpLogins: have " + logins.length + " logins. -----");
+ for (var i = 0; i < logins.length; i++)
+ dumpLogin("login #" + i + " --- ", logins[i]);
+}
+
+function dumpLogin(label, login) {
+ var loginText = "";
+ loginText += "host: ";
+ loginText += login.hostname;
+ loginText += " / formURL: ";
+ loginText += login.formSubmitURL;
+ loginText += " / realm: ";
+ loginText += login.httpRealm;
+ loginText += " / user: ";
+ loginText += login.username;
+ loginText += " / pass: ";
+ loginText += login.password;
+ loginText += " / ufield: ";
+ loginText += login.usernameField;
+ loginText += " / pfield: ";
+ loginText += login.passwordField;
+ ok(true, label + loginText);
+}
+
+function getRecipeParent() {
+ var { LoginManagerParent } = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
+ if (!LoginManagerParent.recipeParentPromise) {
+ return null;
+ }
+ return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
+ return SpecialPowers.wrap(recipeParent);
+ });
+}
+
+/**
+ * Resolves when a specified number of forms have been processed.
+ */
+function promiseFormsProcessed(expectedCount = 1) {
+ var processedCount = 0;
+ return new Promise((resolve, reject) => {
+ function onProcessedForm(subject, topic, data) {
+ processedCount++;
+ if (processedCount == expectedCount) {
+ SpecialPowers.removeObserver(onProcessedForm, "passwordmgr-processed-form");
+ resolve(SpecialPowers.Cu.waiveXrays(subject), data);
+ }
+ }
+ SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form", false);
+ });
+}
+
+function loadRecipes(recipes) {
+ info("Loading recipes");
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("loadedRecipes", function loaded() {
+ chromeScript.removeMessageListener("loadedRecipes", loaded);
+ resolve(recipes);
+ });
+ chromeScript.sendAsyncMessage("loadRecipes", recipes);
+ });
+}
+
+function resetRecipes() {
+ info("Resetting recipes");
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("recipesReset", function reset() {
+ chromeScript.removeMessageListener("recipesReset", reset);
+ resolve();
+ });
+ chromeScript.sendAsyncMessage("resetRecipes");
+ });
+}
+
+function promiseStorageChanged(expectedChangeTypes) {
+ return new Promise((resolve, reject) => {
+ function onStorageChanged({ topic, data }) {
+ let changeType = expectedChangeTypes.shift();
+ is(data, changeType, "Check expected passwordmgr-storage-changed type");
+ if (expectedChangeTypes.length === 0) {
+ chromeScript.removeMessageListener("storageChanged", onStorageChanged);
+ resolve();
+ }
+ }
+ chromeScript.addMessageListener("storageChanged", onStorageChanged);
+ });
+}
+
+function promisePromptShown(expectedTopic) {
+ return new Promise((resolve, reject) => {
+ function onPromptShown({ topic, data }) {
+ is(topic, expectedTopic, "Check expected prompt topic");
+ chromeScript.removeMessageListener("promptShown", onPromptShown);
+ resolve();
+ }
+ chromeScript.addMessageListener("promptShown", onPromptShown);
+ });
+}
+
+/**
+ * Run a function synchronously in the parent process and destroy it in the test cleanup function.
+ * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
+ * or the URL to a JS file.
+ * @return {Object} - the return value of loadChromeScript providing message-related methods.
+ * @see loadChromeScript in specialpowersAPI.js
+ */
+function runInParent(aFunctionOrURL) {
+ let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL);
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.destroy();
+ });
+ return chromeScript;
+}
+
+/**
+ * Run commonInit synchronously in the parent then run the test function after the runTests event.
+ *
+ * @param {Function} aFunction The test function to run
+ */
+function runChecksAfterCommonInit(aFunction = null) {
+ SimpleTest.waitForExplicitFinish();
+ let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+ if (aFunction) {
+ window.addEventListener("runTests", aFunction);
+ pwmgrCommonScript.addMessageListener("registerRunTests", () => registerRunTests());
+ }
+ pwmgrCommonScript.sendSyncMessage("setupParent");
+ return pwmgrCommonScript;
+}
+
+// Code to run when loaded as a chrome script in tests via loadChromeScript
+if (this.addMessageListener) {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ var SpecialPowers = { Cc, Ci, Cr, Cu, };
+ var ok, is;
+ // Ignore ok/is in commonInit since they aren't defined in a chrome script.
+ ok = is = () => {}; // eslint-disable-line no-native-reassign
+
+ Cu.import("resource://gre/modules/LoginHelper.jsm");
+ Cu.import("resource://gre/modules/LoginManagerParent.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ function onStorageChanged(subject, topic, data) {
+ sendAsyncMessage("storageChanged", {
+ topic,
+ data,
+ });
+ }
+ Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed", false);
+
+ function onPrompt(subject, topic, data) {
+ sendAsyncMessage("promptShown", {
+ topic,
+ data,
+ });
+ }
+ Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change", false);
+ Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save", false);
+
+ addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
+ // Force LoginManagerParent to init for the tests since it's normally delayed
+ // by apps such as on Android.
+ LoginManagerParent.init();
+
+ commonInit(selfFilling);
+ sendAsyncMessage("doneSetup");
+ });
+
+ addMessageListener("loadRecipes", Task.async(function*(recipes) {
+ var recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.load(recipes);
+ sendAsyncMessage("loadedRecipes", recipes);
+ }));
+
+ addMessageListener("resetRecipes", Task.async(function*() {
+ let recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.reset();
+ sendAsyncMessage("recipesReset");
+ }));
+
+ addMessageListener("proxyLoginManager", msg => {
+ // Recreate nsILoginInfo objects from vanilla JS objects.
+ let recreatedArgs = msg.args.map((arg, index) => {
+ if (msg.loginInfoIndices.includes(index)) {
+ return LoginHelper.vanillaObjectToLogin(arg);
+ }
+
+ return arg;
+ });
+
+ let rv = Services.logins[msg.methodName](...recreatedArgs);
+ if (rv instanceof Ci.nsILoginInfo) {
+ rv = LoginHelper.loginToVanillaObject(rv);
+ }
+ return rv;
+ });
+
+ var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+ globalMM.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) {
+ sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
+ });
+} else {
+ // Code to only run in the mochitest pages (not in the chrome script).
+ SpecialPowers.pushPrefEnv({"set": [["signon.autofillForms.http", true],
+ ["security.insecure_field_warning.contextual.enabled", false]]
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.popPrefEnv();
+ runInParent(function cleanupParent() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/LoginManagerParent.jsm");
+
+ // Remove all logins and disabled hosts
+ Services.logins.removeAllLogins();
+
+ let disabledHosts = Services.logins.getAllDisabledHosts();
+ disabledHosts.forEach(host => Services.logins.setLoginSavingEnabled(host, true));
+
+ let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].
+ getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ if (LoginManagerParent._recipeManager) {
+ LoginManagerParent._recipeManager.reset();
+ }
+
+ // Cleanup PopupNotifications (if on a relevant platform)
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (chromeWin && chromeWin.PopupNotifications) {
+ let notes = chromeWin.PopupNotifications._currentNotifications;
+ if (notes.length > 0) {
+ dump("Removing " + notes.length + " popup notifications.\n");
+ }
+ for (let note of notes) {
+ note.remove();
+ }
+ }
+ });
+ });
+
+
+ let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {});
+ /**
+ * Proxy for Services.logins (nsILoginManager).
+ * Only supports arguments which support structured clone plus {nsILoginInfo}
+ * Assumes properties are methods.
+ */
+ this.LoginManager = new Proxy({}, {
+ get(target, prop, receiver) {
+ return (...args) => {
+ let loginInfoIndices = [];
+ let cloneableArgs = args.map((val, index) => {
+ if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) {
+ loginInfoIndices.push(index);
+ return LoginHelper.loginToVanillaObject(val);
+ }
+
+ return val;
+ });
+
+ return chromeScript.sendSyncMessage("proxyLoginManager", {
+ args: cloneableArgs,
+ loginInfoIndices,
+ methodName: prop,
+ })[0][0];
+ };
+ },
+ });
+}
diff --git a/toolkit/components/passwordmgr/test/subtst_master_pass.html b/toolkit/components/passwordmgr/test/subtst_master_pass.html
new file mode 100644
index 000000000..20211866a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_master_pass.html
@@ -0,0 +1,12 @@
+<h2>MP subtest</h2>
+This form triggers a MP and gets filled in.<br>
+<form>
+Username: <input type="text" id="userfield" name="u"><br>
+Password: <input type="password" id="passfield" name="p"><br>
+<script>
+ // Only notify when we fill in the password field.
+ document.getElementById("passfield").addEventListener("input", function() {
+ parent.postMessage("filled", "*");
+ });
+</script>
+</form>
diff --git a/toolkit/components/passwordmgr/test/subtst_prompt_async.html b/toolkit/components/passwordmgr/test/subtst_prompt_async.html
new file mode 100644
index 000000000..f60f63814
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_prompt_async.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Multiple auth request</title>
+</head>
+<body>
+ <iframe id="iframe1" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=1&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+ <iframe id="iframe2" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=2&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+ <iframe id="iframe3" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=3&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/test_master_password.html b/toolkit/components/passwordmgr/test/test_master_password.html
new file mode 100644
index 000000000..c8884811f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_master_password.html
@@ -0,0 +1,308 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for master password</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: master password.
+<script>
+"use strict";
+
+commonInit();
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
+ .getService(SpecialPowers.Ci.nsILoginManager);
+var pwcrypt = SpecialPowers.Cc["@mozilla.org/login-manager/crypto/SDR;1"]
+ .getService(Ci.nsILoginManagerCrypto);
+
+var nsLoginInfo = new SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
+
+var exampleCom = "http://example.com/tests/toolkit/components/passwordmgr/test/";
+var exampleOrg = "http://example.org/tests/toolkit/components/passwordmgr/test/";
+
+var login1 = new nsLoginInfo();
+var login2 = new nsLoginInfo();
+
+login1.init("http://example.com", "http://example.com", null,
+ "user1", "pass1", "uname", "pword");
+login2.init("http://example.org", "http://example.org", null,
+ "user2", "pass2", "uname", "pword");
+
+pwmgr.addLogin(login1);
+pwmgr.addLogin(login2);
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+<iframe id="iframe1"></iframe>
+<iframe id="iframe2"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var testNum = 1;
+var iframe1 = document.getElementById("iframe1");
+var iframe2 = document.getElementById("iframe2");
+
+// A couple of tests have to wait until the password manager gets around to
+// filling in the password in the subtest (after we dismiss the master
+// password dialog). In order to accomplish this, the test waits for an event
+// and then posts a message back up to us telling us to continue.
+var continuation = null;
+addEventListener("message", () => {
+ if (continuation) {
+ var c = continuation;
+ continuation = null;
+ c();
+ }
+});
+
+/*
+ * handleDialog
+ *
+ * Invoked a short period of time after calling startCallbackTimer(), and
+ * allows testing the actual auth dialog while it's being displayed. Tests
+ * should call startCallbackTimer() each time the auth dialog is expected (the
+ * timer is a one-shot).
+ */
+function handleDialog(doc, testNumber) {
+ ok(true, "handleDialog running for test " + testNumber);
+
+ var clickOK = true;
+ var doNothing = false;
+ var passfield = doc.getElementById("password1Textbox");
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNumber) {
+ case 1:
+ is(passfield.getAttribute("value"), "", "Checking empty prompt");
+ passfield.setAttribute("value", masterPassword);
+ is(passfield.getAttribute("value"), masterPassword, "Checking filled prompt");
+ break;
+
+ case 2:
+ clickOK = false;
+ break;
+
+ case 3:
+ is(passfield.getAttribute("value"), "", "Checking empty prompt");
+ passfield.setAttribute("value", masterPassword);
+ break;
+
+ case 4:
+ doNothing = true;
+ break;
+
+ case 5:
+ is(passfield.getAttribute("value"), "", "Checking empty prompt");
+ passfield.setAttribute("value", masterPassword);
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNumber);
+ break;
+ }
+
+ didDialog = true;
+
+ if (!doNothing) {
+ SpecialPowers.addObserver(outerWindowObserver, "outer-window-destroyed", false);
+ if (clickOK)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ }
+
+ ok(true, "handleDialog done for test " + testNumber);
+
+ if (testNumber == 4)
+ checkTest4A();
+}
+
+var outerWindowObserver = {
+ observe: function(id) {
+ SpecialPowers.removeObserver(outerWindowObserver, "outer-window-destroyed");
+ var func;
+ if (testNum == 1)
+ func = startTest2;
+ else if (testNum == 2)
+ func = startTest3;
+
+ // For tests 3 and 4C, we use the 'continuation' mechanism, described
+ // above.
+ if (func)
+ setTimeout(func, 300);
+ }
+};
+
+
+function startTest1() {
+ ok(pwcrypt.isLoggedIn, "should be initially logged in (no MP)");
+ enableMasterPassword();
+ ok(!pwcrypt.isLoggedIn, "should be logged out after setting MP");
+
+ // --- Test 1 ---
+ // Trigger a MP prompt via the API
+ startCallbackTimer();
+ var logins = pwmgr.getAllLogins();
+ ok(didDialog, "handleDialog was invoked");
+ is(logins.length, 3, "expected number of logins");
+
+ ok(pwcrypt.isLoggedIn, "should be logged in after MP prompt");
+ logoutMasterPassword();
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+}
+
+function startTest2() {
+ // Try again but click cancel.
+ testNum++;
+ startCallbackTimer();
+ var failedAsExpected = false;
+ logins = null;
+ try {
+ logins = pwmgr.getAllLogins();
+ } catch (e) { failedAsExpected = true; }
+ ok(didDialog, "handleDialog was invoked");
+ ok(failedAsExpected, "getAllLogins should have thrown");
+ is(logins, null, "shouldn't have gotten logins");
+ ok(!pwcrypt.isLoggedIn, "should still be logged out");
+}
+
+function startTest3() {
+ // Load a single iframe to trigger a MP
+ testNum++;
+ iframe1.src = exampleCom + "subtst_master_pass.html";
+ continuation = checkTest3;
+ startCallbackTimer();
+}
+
+function checkTest3() {
+ ok(true, "checkTest3 starting");
+ ok(didDialog, "handleDialog was invoked");
+
+ // check contents of iframe1 fields
+ var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
+ is(u.value, "user1", "checking expected user to have been filled in");
+ is(p.value, "pass1", "checking expected pass to have been filled in");
+
+ ok(pwcrypt.isLoggedIn, "should be logged in");
+ logoutMasterPassword();
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+
+
+ // --- Test 4 ---
+ // first part of loading 2 MP-triggering iframes
+ testNum++;
+ iframe1.src = exampleOrg + "subtst_master_pass.html";
+ // start the callback, but we'll not enter the MP, just call checkTest4A
+ startCallbackTimer();
+}
+
+function checkTest4A() {
+ ok(true, "checkTest4A starting");
+ ok(didDialog, "handleDialog was invoked");
+
+ // check contents of iframe1 fields
+ var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
+ is(u.value, "", "checking expected empty user");
+ is(p.value, "", "checking expected empty pass");
+
+
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+
+ // XXX check that there's 1 MP window open
+
+ // Load another iframe with a login form
+ // This should detect that there's already a pending MP prompt, and not
+ // put up a second one. The load event will fire (note that when pwmgr is
+ // driven from DOMContentLoaded, if that blocks due to prompting for a MP,
+ // the load even will also be blocked until the prompt is dismissed).
+ iframe2.onload = checkTest4B_delay;
+ iframe2.src = exampleCom + "subtst_master_pass.html";
+}
+
+function checkTest4B_delay() {
+ // Testing a negative, wait a little to give the login manager a chance to
+ // (incorrectly) fill in the form. Note, we cannot use setTimeout()
+ // here because the modal window suspends all window timers. Instead we
+ // must use a chrome script to use nsITimer directly.
+ let chromeURL = SimpleTest.getTestFileURL("chrome_timeout.js");
+ let script = SpecialPowers.loadChromeScript(chromeURL);
+ script.addMessageListener('ready', _ => {
+ script.sendAsyncMessage('setTimeout', { delay: 500 });
+ });
+ script.addMessageListener('timeout', checkTest4B);
+}
+
+function checkTest4B() {
+ ok(true, "checkTest4B starting");
+ // iframe2 should load without having triggered a MP prompt (because one
+ // is already waiting)
+
+ // check contents of iframe2 fields
+ var u = SpecialPowers.wrap(iframe2).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe2).contentDocument.getElementById("passfield");
+ is(u.value, "", "checking expected empty user");
+ is(p.value, "", "checking expected empty pass");
+
+ // XXX check that there's 1 MP window open
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+
+ continuation = checkTest4C;
+
+ // Ok, now enter the MP. The MP prompt is already up, but we'll just reuse startCallBackTimer.
+ // --- Test 5 ---
+ testNum++;
+ startCallbackTimer();
+}
+
+function checkTest4C() {
+ ok(true, "checkTest4C starting");
+ ok(didDialog, "handleDialog was invoked");
+
+ // We shouldn't have to worry about iframe1's load event racing with
+ // filling of iframe2's data. We notify observers synchronously, so
+ // iframe2's observer will process iframe2 before iframe1 even finishes
+ // processing the form (which is blocking its load event).
+ ok(pwcrypt.isLoggedIn, "should be logged in");
+
+ // check contents of iframe1 fields
+ var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
+ is(u.value, "user2", "checking expected user to have been filled in");
+ is(p.value, "pass2", "checking expected pass to have been filled in");
+
+ // check contents of iframe2 fields
+ u = SpecialPowers.wrap(iframe2).contentDocument.getElementById("userfield");
+ p = SpecialPowers.wrap(iframe2).contentDocument.getElementById("passfield");
+ is(u.value, "user1", "checking expected user to have been filled in");
+ is(p.value, "pass1", "checking expected pass to have been filled in");
+
+ SimpleTest.finish();
+}
+
+// XXX do a test5ABC with clicking cancel?
+
+SimpleTest.registerCleanupFunction(function finishTest() {
+ disableMasterPassword();
+
+ pwmgr.removeLogin(login1);
+ pwmgr.removeLogin(login2);
+});
+
+window.addEventListener("runTests", startTest1);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/test_prompt_async.html b/toolkit/components/passwordmgr/test/test_prompt_async.html
new file mode 100644
index 000000000..38b34679a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_prompt_async.html
@@ -0,0 +1,540 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Async Auth Prompt</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("untriaged");
+
+ const { NetUtil } = SpecialPowers.Cu.import('resource://gre/modules/NetUtil.jsm');
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+ // Class monitoring number of open dialog windows
+ // It checks there is always open just a single dialog per application
+ function dialogMonitor() {
+ var observerService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ observerService.addObserver(this, "domwindowopened", false);
+ observerService.addObserver(this, "domwindowclosed", false);
+ }
+
+ /*
+ * As documented in Bug 718543, checking equality of objects pulled
+ * from SpecialPowers-wrapped objects is unreliable. Because of that,
+ * `dialogMonitor` now tracks the number of open windows rather than
+ * specific window objects.
+ *
+ * NB: Because the constructor (above) adds |this| directly as an observer,
+ * we need to do SpecialPowers.wrapCallbackObject directly on the prototype.
+ */
+ dialogMonitor.prototype = SpecialPowers.wrapCallbackObject({
+ windowsOpen : 0,
+ windowsRegistered : 0,
+
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIObserver, Ci.nsISupports];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ observe: function(subject, topic, data) {
+ if (topic === "domwindowopened") {
+ this.windowsOpen++;
+ this.windowsRegistered++;
+ return;
+ }
+ if (topic === "domwindowclosed") {
+ this.windowsOpen--;
+ return;
+ }
+ },
+
+ shutdown: function() {
+ var observerService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ observerService.removeObserver(this, "domwindowopened");
+ observerService.removeObserver(this, "domwindowclosed");
+ },
+
+ reset: function() {
+ this.windowsOpen = 0;
+ this.windowsRegistered = 0;
+ }
+ });
+
+ var monitor = new dialogMonitor();
+
+ var pwmgr, logins = [];
+
+ function initLogins(pi) {
+ pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
+ .getService(Ci.nsILoginManager);
+
+ function addLogin(host, realm, user, pass) {
+ var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ login.init(host, null, realm, user, pass, "", "");
+ pwmgr.addLogin(login);
+ logins.push(login);
+ }
+
+ var mozproxy = "moz-proxy://" +
+ SpecialPowers.wrap(pi).host + ":" +
+ SpecialPowers.wrap(pi).port;
+
+ addLogin(mozproxy, "proxy_realm",
+ "proxy_user", "proxy_pass");
+ addLogin(mozproxy, "proxy_realm2",
+ "proxy_user2", "proxy_pass2");
+ addLogin(mozproxy, "proxy_realm3",
+ "proxy_user3", "proxy_pass3");
+ addLogin(mozproxy, "proxy_realm4",
+ "proxy_user4", "proxy_pass4");
+ addLogin(mozproxy, "proxy_realm5",
+ "proxy_user5", "proxy_pass5");
+ addLogin("http://example.com", "mochirealm",
+ "user1name", "user1pass");
+ addLogin("http://example.org", "mochirealm2",
+ "user2name", "user2pass");
+ addLogin("http://example.com", "mochirealm3",
+ "user3name", "user3pass");
+ addLogin("http://example.com", "mochirealm4",
+ "user4name", "user4pass");
+ addLogin("http://example.com", "mochirealm5",
+ "user5name", "user5pass");
+ addLogin("http://example.com", "mochirealm6",
+ "user6name", "user6pass");
+ }
+
+ function finishTest() {
+ ok(true, "finishTest removing testing logins...");
+ for (i in logins)
+ pwmgr.removeLogin(logins[i]);
+
+ var authMgr = SpecialPowers.Cc['@mozilla.org/network/http-auth-manager;1']
+ .getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ monitor.shutdown();
+ SimpleTest.finish();
+ }
+
+ var resolveCallback = SpecialPowers.wrapCallbackObject({
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ onProxyAvailable : function (req, uri, pi, status) {
+ initLogins(pi);
+ doTest(testNum);
+ }
+ });
+
+ function startup() {
+ // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy.
+ var channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true
+ });
+
+ var pps = SpecialPowers.Cc["@mozilla.org/network/protocol-proxy-service;1"]
+ .getService();
+
+ pps.asyncResolve(channel, 0, resolveCallback);
+ }
+
+ // --------------- Test loop spin ----------------
+ var testNum = 1;
+ var iframe1;
+ var iframe2a;
+ var iframe2b;
+ window.onload = function () {
+ iframe1 = document.getElementById("iframe1");
+ iframe2a = document.getElementById("iframe2a");
+ iframe2b = document.getElementById("iframe2b");
+ iframe1.onload = onFrameLoad;
+ iframe2a.onload = onFrameLoad;
+ iframe2b.onload = onFrameLoad;
+
+ startup();
+ };
+
+ var expectedLoads;
+ var expectedDialogs;
+ function onFrameLoad()
+ {
+ if (--expectedLoads == 0) {
+ // All pages expected to load has loaded, continue with the next test
+ ok(true, "Expected frames loaded");
+
+ doCheck(testNum);
+ monitor.reset();
+
+ testNum++;
+ doTest(testNum);
+ }
+ }
+
+ function doTest(testNumber)
+ {
+ /*
+ * These contentDocument variables are located here,
+ * rather than in the global scope, because SpecialPowers threw
+ * errors (complaining that the objects were deleted)
+ * when these were in the global scope.
+ */
+ var iframe1Doc = SpecialPowers.wrap(iframe1).contentDocument;
+ var iframe2aDoc = SpecialPowers.wrap(iframe2a).contentDocument;
+ var iframe2bDoc = SpecialPowers.wrap(iframe2b).contentDocument;
+ var exampleCom = "http://example.com/tests/toolkit/components/passwordmgr/test/";
+ var exampleOrg = "http://example.org/tests/toolkit/components/passwordmgr/test/";
+
+ switch (testNumber)
+ {
+ case 1:
+ // Load through a single proxy with authentication required 3 different
+ // pages, first with one login, other two with their own different login.
+ // We expect to show just a single dialog for proxy authentication and
+ // then two dialogs to authenticate to login 1 and then login 2.
+ ok(true, "doTest testNum 1");
+ expectedLoads = 3;
+ expectedDialogs = 3;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "r=1&" +
+ "user=user1name&" +
+ "pass=user1pass&" +
+ "realm=mochirealm&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ iframe2a.src = exampleOrg + "authenticate.sjs?" +
+ "r=2&" +
+ "user=user2name&" +
+ "pass=user2pass&" +
+ "realm=mochirealm2&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ iframe2b.src = exampleOrg + "authenticate.sjs?" +
+ "r=3&" +
+ "user=user2name&" +
+ "pass=user2pass&" +
+ "realm=mochirealm2&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ break;
+
+ case 2:
+ // Load an iframe with 3 subpages all requiring the same login through
+ // anuthenticated proxy. We expect 2 dialogs, proxy authentication
+ // and web authentication.
+ ok(true, "doTest testNum 2");
+ expectedLoads = 3;
+ expectedDialogs = 2;
+ iframe1.src = exampleCom + "subtst_prompt_async.html";
+ iframe2a.src = "about:blank";
+ iframe2b.src = "about:blank";
+ break;
+
+ case 3:
+ // Load in the iframe page through unauthenticated proxy
+ // and discard the proxy authentication. We expect to see
+ // unauthenticated page content and just a single dialog.
+ ok(true, "doTest testNum 3");
+ expectedLoads = 1;
+ expectedDialogs = 1;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user4name&" +
+ "pass=user4pass&" +
+ "realm=mochirealm4&" +
+ "proxy_user=proxy_user3&" +
+ "proxy_pass=proxy_pass3&" +
+ "proxy_realm=proxy_realm3";
+ break;
+
+ case 4:
+ // Reload the frame from previous step and pass the proxy authentication
+ // but cancel the WWW authentication. We should get the proxy=ok and WWW=fail
+ // content as a result.
+ ok(true, "doTest testNum 4");
+ expectedLoads = 1;
+ expectedDialogs = 2;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user4name&" +
+ "pass=user4pass&" +
+ "realm=mochirealm4&" +
+ "proxy_user=proxy_user3&" +
+ "proxy_pass=proxy_pass3&" +
+ "proxy_realm=proxy_realm3";
+
+
+ break;
+
+ case 5:
+ // Same as the previous two steps but let the server generate
+ // huge content load to check http channel is capable to handle
+ // case when auth dialog is canceled or accepted before unauthenticated
+ // content data is load from the server. (This would be better to
+ // implement using delay of server response).
+ ok(true, "doTest testNum 5");
+ expectedLoads = 1;
+ expectedDialogs = 1;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user5name&" +
+ "pass=user5pass&" +
+ "realm=mochirealm5&" +
+ "proxy_user=proxy_user4&" +
+ "proxy_pass=proxy_pass4&" +
+ "proxy_realm=proxy_realm4&" +
+ "huge=1";
+ break;
+
+ case 6:
+ // Reload the frame from the previous step and let the proxy
+ // authentication pass but WWW fail. We expect two dialogs
+ // and an unathenticated page content load.
+ ok(true, "doTest testNum 6");
+ expectedLoads = 1;
+ expectedDialogs = 2;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user5name&" +
+ "pass=user5pass&" +
+ "realm=mochirealm5&" +
+ "proxy_user=proxy_user4&" +
+ "proxy_pass=proxy_pass4&" +
+ "proxy_realm=proxy_realm4&" +
+ "huge=1";
+ break;
+
+ case 7:
+ // Reload again and let pass all authentication dialogs.
+ // Check we get the authenticated content not broken by
+ // the unauthenticated content.
+ ok(true, "doTest testNum 7");
+ expectedLoads = 1;
+ expectedDialogs = 1;
+ iframe1Doc.location.reload();
+ break;
+
+ case 8:
+ // Check we proccess all challenges sent by server when
+ // user cancels prompts
+ ok(true, "doTest testNum 8");
+ expectedLoads = 1;
+ expectedDialogs = 5;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user6name&" +
+ "pass=user6pass&" +
+ "realm=mochirealm6&" +
+ "proxy_user=proxy_user5&" +
+ "proxy_pass=proxy_pass5&" +
+ "proxy_realm=proxy_realm5&" +
+ "huge=1&" +
+ "multiple=3";
+ break;
+
+ case 9:
+ finishTest();
+ return;
+ }
+
+ startCallbackTimer();
+ }
+
+ function handleDialog(doc, testNumber)
+ {
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNumber)
+ {
+ case 1:
+ case 2:
+ dialog.acceptDialog();
+ break;
+
+ case 3:
+ dialog.cancelDialog();
+ setTimeout(onFrameLoad, 10); // there are no successful frames for test 3
+ break;
+
+ case 4:
+ if (expectedDialogs == 2)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ break;
+
+ case 5:
+ dialog.cancelDialog();
+ setTimeout(onFrameLoad, 10); // there are no successful frames for test 5
+ break;
+
+ case 6:
+ if (expectedDialogs == 2)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ break;
+
+ case 7:
+ dialog.acceptDialog();
+ break;
+
+ case 8:
+ if (expectedDialogs == 3 || expectedDialogs == 1)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ break;
+
+ default:
+ ok(false, "Unhandled testNum " + testNumber + " in handleDialog");
+ }
+
+ if (--expectedDialogs > 0)
+ startCallbackTimer();
+ }
+
+ function doCheck(testNumber)
+ {
+ var iframe1Doc = SpecialPowers.wrap(iframe1).contentDocument;
+ var iframe2aDoc = SpecialPowers.wrap(iframe2a).contentDocument;
+ var iframe2bDoc = SpecialPowers.wrap(iframe2b).contentDocument;
+ var authok1;
+ var proxyok1;
+ var footnote;
+ switch (testNumber)
+ {
+ case 1:
+ ok(true, "doCheck testNum 1");
+ is(monitor.windowsRegistered, 3, "Registered 3 open dialogs");
+
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+
+ var authok2a = iframe2aDoc.getElementById("ok").textContent;
+ var proxyok2a = iframe2aDoc.getElementById("proxy").textContent;
+
+ var authok2b = iframe2bDoc.getElementById("ok").textContent;
+ var proxyok2b = iframe2bDoc.getElementById("proxy").textContent;
+
+ is(authok1, "PASS", "WWW Authorization OK, frame1");
+ is(authok2a, "PASS", "WWW Authorization OK, frame2a");
+ is(authok2b, "PASS", "WWW Authorization OK, frame2b");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(proxyok2a, "PASS", "Proxy Authorization OK, frame2a");
+ is(proxyok2b, "PASS", "Proxy Authorization OK, frame2b");
+ break;
+
+ case 2:
+ is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
+ ok(true, "doCheck testNum 2");
+
+ function checkIframe(frame) {
+ var doc = SpecialPowers.wrap(frame).contentDocument;
+
+ var authok = doc.getElementById("ok").textContent;
+ var proxyok = doc.getElementById("proxy").textContent;
+
+ is(authok, "PASS", "WWW Authorization OK, " + frame.id);
+ is(proxyok, "PASS", "Proxy Authorization OK, " + frame.id);
+ }
+
+ checkIframe(iframe1Doc.getElementById("iframe1"));
+ checkIframe(iframe1Doc.getElementById("iframe2"));
+ checkIframe(iframe1Doc.getElementById("iframe3"));
+ break;
+
+ case 3:
+ ok(true, "doCheck testNum 3");
+ is(monitor.windowsRegistered, 1, "Registered 1 open dialog");
+
+ // ensure that the page content is not displayed on failed proxy auth
+ is(iframe1Doc.getElementById("ok"), null, "frame did not load");
+ break;
+
+ case 4:
+ ok(true, "doCheck testNum 4");
+ is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+
+ is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ break;
+
+ case 5:
+ ok(true, "doCheck testNum 5");
+ is(monitor.windowsRegistered, 1, "Registered 1 open dialog");
+
+ // ensure that the page content is not displayed on failed proxy auth
+ is(iframe1Doc.getElementById("footnote"), null, "frame did not load");
+ break;
+
+ case 6:
+ ok(true, "doCheck testNum 6");
+ is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+ footnote = iframe1Doc.getElementById("footnote").textContent;
+
+ is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ break;
+
+ case 7:
+ ok(true, "doCheck testNum 7");
+ is(monitor.windowsRegistered, 1, "Registered 1 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+ footnote = iframe1Doc.getElementById("footnote").textContent;
+
+ is(authok1, "PASS", "WWW Authorization OK, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ break;
+
+ case 8:
+ ok(true, "doCheck testNum 8");
+ is(monitor.windowsRegistered, 5, "Registered 5 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+ footnote = iframe1Doc.getElementById("footnote").textContent;
+
+ is(authok1, "PASS", "WWW Authorization OK, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ break;
+
+ default:
+ ok(false, "Unhandled testNum " + testNumber + " in doCheck");
+ }
+ }
+
+ </script>
+</head>
+<body>
+ <iframe id="iframe1"></iframe>
+ <iframe id="iframe2a"></iframe>
+ <iframe id="iframe2b"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/test_xhr.html b/toolkit/components/passwordmgr/test/test_xhr.html
new file mode 100644
index 000000000..296371685
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_xhr.html
@@ -0,0 +1,201 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for XHR prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: XHR prompt
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: XHR prompts. **/
+var pwmgr, login1, login2;
+
+function initLogins() {
+ pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://mochi.test:8888", null, "xhr",
+ "xhruser1", "xhrpass1", "", "");
+ login2.init("http://mochi.test:8888", null, "xhr2",
+ "xhruser2", "xhrpass2", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2);
+}
+
+function finishTest() {
+ ok(true, "finishTest removing testing logins...");
+ pwmgr.removeLogin(login1);
+ pwmgr.removeLogin(login2);
+
+ SimpleTest.finish();
+}
+
+function handleDialog(doc, testNum) {
+ ok(true, "handleDialog running for test " + testNum);
+
+ var clickOK = true;
+ var userfield = doc.getElementById("loginTextbox");
+ var passfield = doc.getElementById("password1Textbox");
+ var username = userfield.getAttribute("value");
+ var password = passfield.getAttribute("value");
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNum) {
+ case 1:
+ is(username, "xhruser1", "Checking provided username");
+ is(password, "xhrpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xhruser2", "Checking provided username");
+ is(password, "xhrpass2", "Checking provided password");
+
+ // Check that the dialog is modal, chrome and dependent;
+ // We can't just check window.opener because that'll be
+ // a content window, which therefore isn't exposed (it'll lie and
+ // be null).
+ var win = doc.defaultView;
+ var Ci = SpecialPowers.Ci;
+ var treeOwner = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).treeOwner;
+ treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
+ var flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
+ var wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+ info("Flags: " + flags);
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0,
+ "Dialog should be opened as chrome");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0,
+ "Dialog should be opened as a dialog");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0,
+ "Dialog should be opened as dependent.");
+ ok(wbc.isWindowModal(), "Dialog should be modal");
+
+ // Check that the right tab is focused:
+ var browserWin = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser");
+ var spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+ ok(spec.startsWith("http://mochi.test:8888"),
+ "Tab with remote URI (rather than about:blank) should be focused (" + spec + ")");
+
+
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ // Explicitly cancel the dialog and report a fail in this failure
+ // case, rather than letting the dialog get stuck due to an auth
+ // failure and having the test timeout.
+ if (!username && !password) {
+ ok(false, "No values prefilled");
+ clickOK = false;
+ }
+
+ if (clickOK)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+
+ ok(true, "handleDialog done");
+ didDialog = true;
+}
+
+var newWin;
+function xhrLoad(xmlDoc) {
+ ok(true, "xhrLoad running for test " + testNum);
+
+ // The server echos back the user/pass it received.
+ var username = xmlDoc.getElementById("user").textContent;
+ var password = xmlDoc.getElementById("pass").textContent;
+ var authok = xmlDoc.getElementById("ok").textContent;
+
+
+ switch (testNum) {
+ case 1:
+ is(username, "xhruser1", "Checking provided username");
+ is(password, "xhrpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xhruser2", "Checking provided username");
+ is(password, "xhrpass2", "Checking provided password");
+
+ newWin.close();
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ doTest();
+}
+
+function doTest() {
+ switch (++testNum) {
+ case 1:
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xhruser1&pass=xhrpass1&realm=xhr");
+ break;
+
+ case 2:
+ // Test correct parenting, by opening another tab in the foreground,
+ // and making sure the prompt re-focuses the original tab when shown:
+ newWin = window.open();
+ newWin.focus();
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xhruser2&pass=xhrpass2&realm=xhr2");
+ break;
+
+ default:
+ finishTest();
+ }
+}
+
+function makeRequest(uri) {
+ var request = new XMLHttpRequest();
+ request.open("GET", uri, true);
+ request.onreadystatechange = function () {
+ if (request.readyState == 4)
+ xhrLoad(request.responseXML);
+ };
+ request.send(null);
+}
+
+
+initLogins();
+
+// clear plain HTTP auth sessions before the test, to allow
+// running them more than once.
+var authMgr = SpecialPowers.Cc['@mozilla.org/network/http-auth-manager;1']
+ .getService(SpecialPowers.Ci.nsIHttpAuthManager);
+authMgr.clearAll();
+
+// start the tests
+testNum = 0;
+doTest();
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/test_xml_load.html b/toolkit/components/passwordmgr/test/test_xml_load.html
new file mode 100644
index 000000000..5672c7117
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_xml_load.html
@@ -0,0 +1,191 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test XML document prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: XML prompt
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: XML prompts. **/
+var pwmgr, login1, login2;
+
+function initLogins() {
+ pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
+ .getService(Ci.nsILoginManager);
+
+ login1 = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ login2 = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://mochi.test:8888", null, "xml",
+ "xmluser1", "xmlpass1", "", "");
+ login2.init("http://mochi.test:8888", null, "xml2",
+ "xmluser2", "xmlpass2", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2);
+}
+
+function handleDialog(doc, testNum) {
+ ok(true, "handleDialog running for test " + testNum);
+
+ var clickOK = true;
+ var userfield = doc.getElementById("loginTextbox");
+ var passfield = doc.getElementById("password1Textbox");
+ var username = userfield.getAttribute("value");
+ var password = passfield.getAttribute("value");
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNum) {
+ case 1:
+ is(username, "xmluser1", "Checking provided username");
+ is(password, "xmlpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xmluser2", "Checking provided username");
+ is(password, "xmlpass2", "Checking provided password");
+
+ // Check that the dialog is modal, chrome and dependent;
+ // We can't just check window.opener because that'll be
+ // a content window, which therefore isn't exposed (it'll lie and
+ // be null).
+ var win = doc.defaultView;
+ var Ci = SpecialPowers.Ci;
+ var treeOwner = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).treeOwner;
+ treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
+ var flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
+ var wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+ info("Flags: " + flags);
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0,
+ "Dialog should be opened as chrome");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0,
+ "Dialog should be opened as a dialog");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0,
+ "Dialog should be opened as dependent.");
+ ok(wbc.isWindowModal(), "Dialog should be modal");
+
+ // Check that the right tab is focused:
+ var browserWin = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser");
+ var spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+ ok(spec.startsWith("http://mochi.test:8888"),
+ "Tab with remote URI (rather than about:blank) should be focused (" + spec + ")");
+
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ // Explicitly cancel the dialog and report a fail in this failure
+ // case, rather than letting the dialog get stuck due to an auth
+ // failure and having the test timeout.
+ if (!username && !password) {
+ ok(false, "No values prefilled");
+ clickOK = false;
+ }
+
+ if (clickOK)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+
+ ok(true, "handleDialog done");
+ didDialog = true;
+}
+
+var newWin;
+function xmlLoad(responseDoc) {
+ ok(true, "xmlLoad running for test " + testNum);
+
+ // The server echos back the user/pass it received.
+ var username = responseDoc.getElementById("user").textContent;
+ var password = responseDoc.getElementById("pass").textContent;
+ var authok = responseDoc.getElementById("ok").textContent;
+
+ switch (testNum) {
+ case 1:
+ is(username, "xmluser1", "Checking provided username");
+ is(password, "xmlpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xmluser2", "Checking provided username");
+ is(password, "xmlpass2", "Checking provided password");
+
+ newWin.close();
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ doTest();
+}
+
+function doTest() {
+ switch (++testNum) {
+ case 1:
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xmluser1&pass=xmlpass1&realm=xml");
+ break;
+
+ case 2:
+ // Test correct parenting, by opening another tab in the foreground,
+ // and making sure the prompt re-focuses the original tab when shown:
+ newWin = window.open();
+ newWin.focus();
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xmluser2&pass=xmlpass2&realm=xml2");
+ break;
+
+ default:
+ SimpleTest.finish();
+ }
+}
+
+function makeRequest(uri) {
+ var xmlDoc = document.implementation.createDocument("", "test", null);
+
+ function documentLoaded(e) {
+ xmlLoad(xmlDoc);
+ }
+ xmlDoc.addEventListener("load", documentLoaded, false);
+ xmlDoc.load(uri);
+}
+
+
+initLogins();
+
+// clear plain HTTP auth sessions before the test, to allow
+// running them more than once.
+var authMgr = SpecialPowers.Cc['@mozilla.org/network/http-auth-manager;1']
+ .getService(SpecialPowers.Ci.nsIHttpAuthManager);
+authMgr.clearAll();
+
+// start the tests
+testNum = 0;
+doTest();
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/unit/.eslintrc.js b/toolkit/components/passwordmgr/test/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite b/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite
new file mode 100644
index 000000000..b234246ca
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/key3.db b/toolkit/components/passwordmgr/test/unit/data/key3.db
new file mode 100644
index 000000000..a83a0a577
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/key3.db
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite
new file mode 100644
index 000000000..fe030b61f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite
new file mode 100644
index 000000000..729512a12
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite
new file mode 100644
index 000000000..a6c72b31e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite
new file mode 100644
index 000000000..359df5d31
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite
new file mode 100644
index 000000000..918f4142f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite
new file mode 100644
index 000000000..e06c33aae
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite
new file mode 100644
index 000000000..227c09c81
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite
new file mode 100644
index 000000000..4534cf255
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite
new file mode 100644
index 000000000..eb4ee6d01
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite
new file mode 100644
index 000000000..e09c4f710
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite
new file mode 100644
index 000000000..0328a1a02
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/head.js b/toolkit/components/passwordmgr/test/unit/head.js
new file mode 100644
index 000000000..baf958ab4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/head.js
@@ -0,0 +1,135 @@
+/**
+ * Provides infrastructure for automated login components tests.
+ */
+
+"use strict";
+
+// Globals
+
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoginRecipes.jsm");
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+Cu.import("resource://testing-common/MockDocument.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+const LoginInfo =
+ Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo", "init");
+
+// Import LoginTestUtils.jsm as LoginTestUtils.
+XPCOMUtils.defineLazyModuleGetter(this, "LoginTestUtils",
+ "resource://testing-common/LoginTestUtils.jsm");
+LoginTestUtils.Assert = Assert;
+const TestData = LoginTestUtils.testData;
+const newPropertyBag = LoginHelper.newPropertyBag;
+
+/**
+ * All the tests are implemented with add_task, this starts them automatically.
+ */
+function run_test()
+{
+ do_get_profile();
+ run_next_test();
+}
+
+// Global helpers
+
+// Some of these functions are already implemented in other parts of the source
+// tree, see bug 946708 about sharing more code.
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+let gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ do_check_false(file.exists());
+
+ do_register_cleanup(function () {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+
+ return file;
+}
+
+const RecipeHelpers = {
+ initNewParent() {
+ return (new LoginRecipesParent({ defaults: null })).initializationPromise;
+ },
+};
+
+// Initialization functions common to all tests
+
+add_task(function* test_common_initialize()
+{
+ // Before initializing the service for the first time, we should copy the key
+ // file required to decrypt the logins contained in the SQLite databases used
+ // by migration tests. This file is not required for the other tests.
+ yield OS.File.copy(do_get_file("data/key3.db").path,
+ OS.Path.join(OS.Constants.Path.profileDir, "key3.db"));
+
+ // Ensure that the service and the storage module are initialized.
+ yield Services.logins.initializationPromise;
+
+ // Ensure that every test file starts with an empty database.
+ LoginTestUtils.clearData();
+
+ // Clean up after every test.
+ do_register_cleanup(() => LoginTestUtils.clearData());
+});
+
+/**
+ * Compare two FormLike to see if they represent the same information. Elements
+ * are compared using their @id attribute.
+ */
+function formLikeEqual(a, b) {
+ Assert.strictEqual(Object.keys(a).length, Object.keys(b).length,
+ "Check the formLikes have the same number of properties");
+
+ for (let propName of Object.keys(a)) {
+ if (propName == "elements") {
+ Assert.strictEqual(a.elements.length, b.elements.length, "Check element count");
+ for (let i = 0; i < a.elements.length; i++) {
+ Assert.strictEqual(a.elements[i].id, b.elements[i].id, "Check element " + i + " id");
+ }
+ continue;
+ }
+ Assert.strictEqual(a[propName], b[propName], "Compare formLike " + propName + " property");
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js b/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js
new file mode 100644
index 000000000..94d2e50c0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js
@@ -0,0 +1,75 @@
+/**
+ * Tests the OSCrypto object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
+ "resource://gre/modules/OSCrypto.jsm");
+
+var crypto = new OSCrypto();
+
+// Tests
+
+add_task(function test_getIELoginHash()
+{
+ do_check_eq(crypto.getIELoginHash("https://bugzilla.mozilla.org/page.cgi"),
+ "4A66FE96607885790F8E67B56EEE52AB539BAFB47D");
+
+ do_check_eq(crypto.getIELoginHash("https://github.com/login"),
+ "0112F7DCE67B8579EA01367678AA44AB9868B5A143");
+
+ do_check_eq(crypto.getIELoginHash("https://login.live.com/login.srf"),
+ "FBF92E5D804C82717A57856533B779676D92903688");
+
+ do_check_eq(crypto.getIELoginHash("https://preview.c9.io/riadh/w1/pass.1.html"),
+ "6935CF27628830605927F86AB53831016FC8973D1A");
+
+
+ do_check_eq(crypto.getIELoginHash("https://reviewboard.mozilla.org/account/login/"),
+ "09141FD287E2E59A8B1D3BB5671537FD3D6B61337A");
+
+ do_check_eq(crypto.getIELoginHash("https://www.facebook.com/"),
+ "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796");
+
+});
+
+add_task(function test_decryptData_encryptData()
+{
+ function decryptEncryptTest(key) {
+ do_check_eq(crypto.decryptData(crypto.encryptData("", key), key),
+ "");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("secret", key), key),
+ "secret");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("https://www.mozilla.org", key),
+ key),
+ "https://www.mozilla.org");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("https://reviewboard.mozilla.org", key),
+ key),
+ "https://reviewboard.mozilla.org");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("https://bugzilla.mozilla.org/page.cgi",
+ key),
+ key),
+ "https://bugzilla.mozilla.org/page.cgi");
+ }
+
+ let keys = [null, "a", "keys", "abcdedf", "pass", "https://bugzilla.mozilla.org/page.cgi",
+ "https://login.live.com/login.srf"];
+ for (let key of keys) {
+ decryptEncryptTest(key);
+ }
+ let url = "https://twitter.com/";
+ let value = [1, 0, 0, 0, 208, 140, 157, 223, 1, 21, 209, 17, 140, 122, 0, 192, 79, 194, 151, 235, 1, 0, 0, 0, 254, 58, 230, 75, 132, 228, 181, 79, 184, 160, 37, 106, 201, 29, 42, 152, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 16, 102, 0, 0, 0, 1, 0, 0, 32, 0, 0, 0, 90, 136, 17, 124, 122, 57, 178, 24, 34, 86, 209, 198, 184, 107, 58, 58, 32, 98, 61, 239, 129, 101, 56, 239, 114, 159, 139, 165, 183, 40, 183, 85, 0, 0, 0, 0, 14, 128, 0, 0, 0, 2, 0, 0, 32, 0, 0, 0, 147, 170, 34, 21, 53, 227, 191, 6, 201, 84, 106, 31, 57, 227, 46, 127, 219, 199, 80, 142, 37, 104, 112, 223, 26, 165, 223, 55, 176, 89, 55, 37, 112, 0, 0, 0, 98, 70, 221, 109, 5, 152, 46, 11, 190, 213, 226, 58, 244, 20, 180, 217, 63, 155, 227, 132, 7, 151, 235, 6, 37, 232, 176, 182, 141, 191, 251, 50, 20, 123, 53, 11, 247, 233, 112, 121, 130, 27, 168, 68, 92, 144, 192, 7, 12, 239, 53, 217, 253, 155, 54, 109, 236, 216, 225, 245, 79, 234, 165, 225, 104, 36, 77, 13, 195, 237, 143, 165, 100, 107, 230, 70, 54, 19, 179, 35, 8, 101, 93, 202, 121, 210, 222, 28, 93, 122, 36, 84, 185, 249, 238, 3, 102, 149, 248, 94, 137, 16, 192, 22, 251, 220, 22, 223, 16, 58, 104, 187, 64, 0, 0, 0, 70, 72, 15, 119, 144, 66, 117, 203, 190, 82, 131, 46, 111, 130, 238, 191, 170, 63, 186, 117, 46, 88, 171, 3, 94, 146, 75, 86, 243, 159, 63, 195, 149, 25, 105, 141, 42, 217, 108, 18, 63, 62, 98, 182, 241, 195, 12, 216, 152, 230, 176, 253, 202, 129, 41, 185, 135, 111, 226, 92, 27, 78, 27, 198];
+
+ let arr1 = crypto.arrayToString(value);
+ let arr2 = crypto.stringToArray(crypto.decryptData(crypto.encryptData(arr1, url), url));
+ for (let i = 0; i < arr1.length; i++) {
+ do_check_eq(arr2[i], value[i]);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_context_menu.js b/toolkit/components/passwordmgr/test/unit/test_context_menu.js
new file mode 100644
index 000000000..722c13e15
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_context_menu.js
@@ -0,0 +1,165 @@
+/*
+ * Test the password manager context menu.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoginManagerContextMenu.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
+ return Services.strings.
+ createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
+
+/**
+ * Prepare data for the following tests.
+ */
+add_task(function* test_initialize() {
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Tests if the LoginManagerContextMenu returns the correct login items.
+ */
+add_task(function* test_contextMenuAddAndRemoveLogins() {
+ const DOCUMENT_CONTENT = "<form><input id='pw' type=password></form>";
+ const INPUT_QUERY = "input[type='password']";
+
+ let testHostnames = [
+ "http://www.example.com",
+ "http://www2.example.com",
+ "http://www3.example.com",
+ "http://empty.example.com",
+ ];
+
+ for (let hostname of testHostnames) {
+ do_print("test for hostname: " + hostname);
+ // Get expected logins for this test.
+ let logins = getExpectedLogins(hostname);
+
+ // Create the logins menuitems fragment.
+ let {fragment, document} = createLoginsFragment(hostname, DOCUMENT_CONTENT, INPUT_QUERY);
+
+ if (!logins.length) {
+ Assert.ok(fragment === null, "Null returned. No logins where found.");
+ continue;
+ }
+ let items = [...fragment.querySelectorAll("menuitem")];
+
+ // Check if the items are those expected to be listed.
+ Assert.ok(checkLoginItems(logins, items), "All expected logins found.");
+ document.body.appendChild(fragment);
+
+ // Try to clear the fragment.
+ LoginManagerContextMenu.clearLoginsFromMenu(document);
+ Assert.equal(fragment.querySelectorAll("menuitem").length, 0, "All items correctly cleared.");
+ }
+
+ Services.logins.removeAllLogins();
+});
+
+/**
+ * Create a fragment with a menuitem for each login.
+ */
+function createLoginsFragment(url, content, elementQuery) {
+ const CHROME_URL = "chrome://mock-chrome";
+
+ // Create a mock document.
+ let document = MockDocument.createTestDocument(CHROME_URL, content);
+ let inputElement = document.querySelector(elementQuery);
+ MockDocument.mockOwnerDocumentProperty(inputElement, document, url);
+
+ // We also need a simple mock Browser object for this test.
+ let browser = {
+ ownerDocument: document
+ };
+
+ let URI = Services.io.newURI(url, null, null);
+ return {
+ document,
+ fragment: LoginManagerContextMenu.addLoginsToMenu(inputElement, browser, URI),
+ };
+}
+
+/**
+ * Check if every login have it's corresponding menuitem.
+ * Duplicates and empty usernames have a date appended.
+ */
+function checkLoginItems(logins, items) {
+ function findDuplicates(unfilteredLoginList) {
+ var seen = new Set();
+ var duplicates = new Set();
+ for (let login of unfilteredLoginList) {
+ if (seen.has(login.username)) {
+ duplicates.add(login.username);
+ }
+ seen.add(login.username);
+ }
+ return duplicates;
+ }
+ let duplicates = findDuplicates(logins);
+
+ let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+ for (let login of logins) {
+ if (login.username && !duplicates.has(login.username)) {
+ // If login is not duplicate and we can't find an item for it, fail.
+ if (!items.find(item => item.label == login.username)) {
+ return false;
+ }
+ continue;
+ }
+
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+ // If login is duplicate, check if we have a login item with appended date.
+ if (login.username && !items.find(item => item.label == login.username + " (" + time + ")")) {
+ return false;
+ }
+ // If login is empty, check if we have a login item with appended date.
+ if (!login.username &&
+ !items.find(item => item.label == _stringBundle.GetStringFromName("noUsername") + " (" + time + ")")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Gets the list of expected logins for a hostname.
+ */
+function getExpectedLogins(hostname) {
+ return Services.logins.getAllLogins().filter(entry => entry["hostname"] === hostname);
+}
+
+function loginList() {
+ return [
+ new LoginInfo("http://www.example.com", "http://www.example.com", null,
+ "username1", "password",
+ "form_field_username", "form_field_password"),
+
+ new LoginInfo("http://www.example.com", "http://www.example.com", null,
+ "username2", "password",
+ "form_field_username", "form_field_password"),
+
+ new LoginInfo("http://www2.example.com", "http://www.example.com", null,
+ "username", "password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www2.example.com", "http://www2.example.com", null,
+ "username", "password2",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www2.example.com", "http://www2.example.com", null,
+ "username2", "password2",
+ "form_field_username", "form_field_password"),
+
+ new LoginInfo("http://www3.example.com", "http://www.example.com", null,
+ "", "password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www3.example.com", "http://www3.example.com", null,
+ "", "password2",
+ "form_field_username", "form_field_password"),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js b/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js
new file mode 100644
index 000000000..d688a6dbf
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js
@@ -0,0 +1,284 @@
+/*
+ * Test LoginHelper.dedupeLogins
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+
+const DOMAIN1_HTTP_TO_HTTP_U1_P1 = TestData.formLogin({
+ timePasswordChanged: 3000,
+ timeLastUsed: 2000,
+});
+const DOMAIN1_HTTP_TO_HTTP_U1_P2 = TestData.formLogin({
+ password: "password two",
+});
+const DOMAIN1_HTTP_TO_HTTP_U2_P2 = TestData.formLogin({
+ password: "password two",
+ username: "username two",
+});
+const DOMAIN1_HTTPS_TO_HTTPS_U1_P1 = TestData.formLogin({
+ formSubmitURL: "http://www.example.com",
+ hostname: "https://www3.example.com",
+ timePasswordChanged: 4000,
+ timeLastUsed: 1000,
+});
+const DOMAIN1_HTTPS_TO_EMPTY_U1_P1 = TestData.formLogin({
+ formSubmitURL: "",
+ hostname: "https://www3.example.com",
+});
+const DOMAIN1_HTTPS_TO_EMPTYU_P1 = TestData.formLogin({
+ hostname: "https://www3.example.com",
+ username: "",
+});
+const DOMAIN1_HTTP_AUTH = TestData.authLogin({
+ hostname: "http://www3.example.com",
+});
+const DOMAIN1_HTTPS_AUTH = TestData.authLogin({
+ hostname: "https://www3.example.com",
+});
+
+
+add_task(function test_dedupeLogins() {
+ // [description, expectedOutput, dedupe arg. 0, dedupe arg 1, ...]
+ let testcases = [
+ [
+ "exact dupes",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ [], // force no resolveBy logic to test behavior of preferring the first..
+ ],
+ [
+ "default uniqueKeys is un + pw",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
+ undefined,
+ [],
+ ],
+ [
+ "same usernames, different passwords, dedupe username only",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
+ ["username"],
+ [],
+ ],
+ [
+ "same un+pw, different scheme",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ [],
+ ],
+ [
+ "same un+pw, different scheme, reverse order",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ [],
+ ],
+ [
+ "same un+pw, different scheme, include hostname",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ ["hostname", "username", "password"],
+ [],
+ ],
+ [
+ "empty username is not deduped with non-empty",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1],
+ undefined,
+ [],
+ ],
+ [
+ "empty username is deduped with same passwords",
+ [DOMAIN1_HTTPS_TO_EMPTYU_P1],
+ [DOMAIN1_HTTPS_TO_EMPTYU_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ ["password"],
+ [],
+ ],
+ [
+ "mix of form and HTTP auth",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_AUTH],
+ undefined,
+ [],
+ ],
+ ];
+
+ for (let tc of testcases) {
+ let description = tc.shift();
+ let expected = tc.shift();
+ let actual = LoginHelper.dedupeLogins(...tc);
+ Assert.strictEqual(actual.length, expected.length, `Check: ${description}`);
+ for (let [i, login] of expected.entries()) {
+ Assert.strictEqual(actual[i], login, `Check index ${i}`);
+ }
+ }
+});
+
+
+add_task(function* test_dedupeLogins_resolveBy() {
+ Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timeLastUsed > DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timeLastUsed,
+ "Sanity check timeLastUsed difference");
+ Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timePasswordChanged < DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timePasswordChanged,
+ "Sanity check timePasswordChanged difference");
+
+ let testcases = [
+ [
+ "default resolveBy is timeLastUsed",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ ],
+ [
+ "default resolveBy is timeLastUsed, reversed input",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ ],
+ [
+ "resolveBy timeLastUsed + timePasswordChanged",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timeLastUsed", "timePasswordChanged"],
+ ],
+ [
+ "resolveBy timeLastUsed + timePasswordChanged, reversed input",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["timeLastUsed", "timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged, reversed",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged + timeLastUsed",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timePasswordChanged", "timeLastUsed"],
+ ],
+ [
+ "resolveBy timePasswordChanged + timeLastUsed, reversed",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["timePasswordChanged", "timeLastUsed"],
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTP",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTP, reversed input",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTPS",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTPS, reversed input",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme HTTP auth",
+ [DOMAIN1_HTTPS_AUTH],
+ [DOMAIN1_HTTP_AUTH, DOMAIN1_HTTPS_AUTH],
+ undefined,
+ ["scheme"],
+ DOMAIN1_HTTPS_AUTH.hostname,
+ ],
+ [
+ "resolveBy scheme HTTP auth, reversed input",
+ [DOMAIN1_HTTPS_AUTH],
+ [DOMAIN1_HTTPS_AUTH, DOMAIN1_HTTP_AUTH],
+ undefined,
+ ["scheme"],
+ DOMAIN1_HTTPS_AUTH.hostname,
+ ],
+ [
+ "resolveBy scheme, empty form submit URL",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTPS_TO_EMPTY_U1_P1],
+ undefined,
+ ["scheme"],
+ DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
+ ],
+ ];
+
+ for (let tc of testcases) {
+ let description = tc.shift();
+ let expected = tc.shift();
+ let actual = LoginHelper.dedupeLogins(...tc);
+ Assert.strictEqual(actual.length, expected.length, `Check: ${description}`);
+ for (let [i, login] of expected.entries()) {
+ Assert.strictEqual(actual[i], login, `Check index ${i}`);
+ }
+ }
+
+});
+
+add_task(function* test_dedupeLogins_preferredOriginMissing() {
+ let testcases = [
+ [
+ "resolveBy scheme + timePasswordChanged, missing preferredOrigin",
+ /preferredOrigin/,
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged + scheme, missing preferredOrigin",
+ /preferredOrigin/,
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timePasswordChanged", "scheme"],
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, empty preferredOrigin",
+ /preferredOrigin/,
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ "",
+ ],
+ ];
+
+ for (let tc of testcases) {
+ let description = tc.shift();
+ let expectedException = tc.shift();
+ Assert.throws(() => {
+ LoginHelper.dedupeLogins(...tc);
+ }, expectedException, `Check: ${description}`);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js b/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js
new file mode 100644
index 000000000..ff3b7e868
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js
@@ -0,0 +1,196 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests getLoginSavingEnabled, setLoginSavingEnabled, and getAllDisabledHosts.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Tests setLoginSavingEnabled and getAllDisabledHosts.
+ */
+add_task(function test_setLoginSavingEnabled_getAllDisabledHosts()
+{
+ // Add some disabled hosts, and verify that different schemes for the same
+ // domain are considered different hosts.
+ let hostname1 = "http://disabled1.example.com";
+ let hostname2 = "http://disabled2.example.com";
+ let hostname3 = "https://disabled2.example.com";
+ Services.logins.setLoginSavingEnabled(hostname1, false);
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+ Services.logins.setLoginSavingEnabled(hostname3, false);
+
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1, hostname2, hostname3]);
+
+ // Adding the same host twice should not result in an error.
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1, hostname2, hostname3]);
+
+ // Removing a disabled host should work.
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1, hostname3]);
+
+ // Removing the last disabled host should work.
+ Services.logins.setLoginSavingEnabled(hostname1, true);
+ Services.logins.setLoginSavingEnabled(hostname3, true);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ []);
+});
+
+/**
+ * Tests setLoginSavingEnabled and getLoginSavingEnabled.
+ */
+add_task(function test_setLoginSavingEnabled_getLoginSavingEnabled()
+{
+ let hostname1 = "http://disabled.example.com";
+ let hostname2 = "https://disabled.example.com";
+
+ // Hosts should not be disabled by default.
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Test setting initial values.
+ Services.logins.setLoginSavingEnabled(hostname1, false);
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Test changing values.
+ Services.logins.setLoginSavingEnabled(hostname1, true);
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Clean up.
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+});
+
+/**
+ * Tests setLoginSavingEnabled with invalid NUL characters in the hostname.
+ */
+add_task(function test_setLoginSavingEnabled_invalid_characters()
+{
+ let hostname = "http://null\0X.example.com";
+ Assert.throws(() => Services.logins.setLoginSavingEnabled(hostname, false),
+ /Invalid hostname/);
+
+ // Verify that no data was stored by the previous call.
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ []);
+});
+
+/**
+ * Tests different values of the "signon.rememberSignons" property.
+ */
+add_task(function test_rememberSignons()
+{
+ let hostname1 = "http://example.com";
+ let hostname2 = "http://localhost";
+
+ // The default value for the preference should be true.
+ do_check_true(Services.prefs.getBoolPref("signon.rememberSignons"));
+
+ // Hosts should not be disabled by default.
+ Services.logins.setLoginSavingEnabled(hostname1, false);
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Disable storage of saved passwords globally.
+ Services.prefs.setBoolPref("signon.rememberSignons", false);
+ do_register_cleanup(
+ () => Services.prefs.clearUserPref("signon.rememberSignons"));
+
+ // All hosts should now appear disabled.
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // The list of disabled hosts should be unaltered.
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1]);
+
+ // Changing values with the preference set should work.
+ Services.logins.setLoginSavingEnabled(hostname1, true);
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+
+ // All hosts should still appear disabled.
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // The list of disabled hosts should have been changed.
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname2]);
+
+ // Enable storage of saved passwords again.
+ Services.prefs.setBoolPref("signon.rememberSignons", true);
+
+ // Hosts should now appear enabled as requested.
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Clean up.
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ []);
+});
+
+/**
+ * Tests storing disabled hosts with non-ASCII characters where IDN is supported.
+ */
+add_task(function* test_storage_setLoginSavingEnabled_nonascii_IDN_is_supported()
+{
+ let hostname = "http://大.net";
+ let encoding = "http://xn--pss.net";
+
+ // Test adding disabled host with nonascii URL (http://大.net).
+ Services.logins.setLoginSavingEnabled(hostname, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [hostname]);
+
+ LoginTestUtils.clearData();
+
+ // Test adding disabled host with IDN ("http://xn--pss.net").
+ Services.logins.setLoginSavingEnabled(encoding, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [hostname]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests storing disabled hosts with non-ASCII characters where IDN is not supported.
+ */
+add_task(function* test_storage_setLoginSavingEnabled_nonascii_IDN_not_supported()
+{
+ let hostname = "http://√.com";
+ let encoding = "http://xn--19g.com";
+
+ // Test adding disabled host with nonascii URL (http://√.com).
+ Services.logins.setLoginSavingEnabled(hostname, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [encoding]);
+
+ LoginTestUtils.clearData();
+
+ // Test adding disabled host with IDN ("http://xn--19g.com").
+ Services.logins.setLoginSavingEnabled(encoding, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [encoding]);
+
+ LoginTestUtils.clearData();
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_getFormFields.js b/toolkit/components/passwordmgr/test/unit/test_getFormFields.js
new file mode 100644
index 000000000..46912ab8f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_getFormFields.js
@@ -0,0 +1,147 @@
+/*
+ * Test for LoginManagerContent._getFormFields.
+ */
+
+"use strict";
+
+// Services.prefs.setBoolPref("signon.debug", true);
+
+Cu.importGlobalProperties(["URL"]);
+
+const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+const TESTCASES = [
+ {
+ description: "1 password field outside of a <form>",
+ document: `<input id="pw1" type=password>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 text field outside of a <form> without a password field",
+ document: `<input id="un1">`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 username & password field outside of a <form>",
+ document: `<input id="un1">
+ <input id="pw1" type=password>`,
+ returnedFieldIDs: ["un1", "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 username & password field in a <form>",
+ document: `<form>
+ <input id="un1">
+ <input id="pw1" type=password>
+ </form>`,
+ returnedFieldIDs: ["un1", "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 empty password fields outside of a <form>",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password>
+ <input id="pw3" type=password>
+ <input id="pw4" type=password>`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 password fields outside of a <form> (1 empty, 3 full) with skipEmpty",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password value="pass2">
+ <input id="pw3" type=password value="pass3">
+ <input id="pw4" type=password value="pass4">`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: true,
+ },
+ {
+ description: "Form with 1 password field",
+ document: `<form><input id="pw1" type=password></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "Form with 2 password fields",
+ document: `<form><input id="pw1" type=password><input id='pw2' type=password></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field in a form, 1 outside (not processed)",
+ document: `<form><input id="pw1" type=password></form><input id="pw2" type=password>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field in a form, 1 text field outside (not processed)",
+ document: `<form><input id="pw1" type=password></form><input>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 text field in a form, 1 password field outside (not processed)",
+ document: `<form><input></form><input id="pw1" type=password>`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form='form1'>
+ <form id="form1"></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: true,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty with 1 empty",
+ document: `<input id="pw1" type=password value="pass1"><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: true,
+ },
+];
+
+for (let tc of TESTCASES) {
+ do_print("Sanity checking the testcase: " + tc.description);
+
+ (function() {
+ let testcase = tc;
+ add_task(function*() {
+ do_print("Starting testcase: " + testcase.description);
+ let document = MockDocument.createTestDocument("http://localhost:8080/test/",
+ testcase.document);
+
+ let input = document.querySelector("input");
+ MockDocument.mockOwnerDocumentProperty(input, document, "http://localhost:8080/test/");
+
+ let formLike = LoginFormFactory.createFromField(input);
+
+ let actual = LoginManagerContent._getFormFields(formLike,
+ testcase.skipEmptyFields,
+ new Set());
+
+ Assert.strictEqual(testcase.returnedFieldIDs.length, 3,
+ "_getFormFields returns 3 elements");
+
+ for (let i = 0; i < testcase.returnedFieldIDs.length; i++) {
+ let expectedID = testcase.returnedFieldIDs[i];
+ if (expectedID === null) {
+ Assert.strictEqual(actual[i], expectedID,
+ "Check returned field " + i + " is null");
+ } else {
+ Assert.strictEqual(actual[i].id, expectedID,
+ "Check returned field " + i + " ID");
+ }
+ }
+ });
+ })();
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js b/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js
new file mode 100644
index 000000000..08fa422ab
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js
@@ -0,0 +1,156 @@
+/*
+ * Test for LoginManagerContent._getPasswordFields using LoginFormFactory.
+ */
+
+"use strict";
+
+const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+const TESTCASES = [
+ {
+ description: "Empty document",
+ document: ``,
+ returnedFieldIDsByFormLike: [],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "Non-password input with no <form> present",
+ document: `<input>`,
+ // Only the IDs of password fields should be in this array
+ returnedFieldIDsByFormLike: [[]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field outside of a <form>",
+ document: `<input id="pw1" type=password>`,
+ returnedFieldIDsByFormLike: [["pw1"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 empty password fields outside of a <form>",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password>
+ <input id="pw3" type=password>
+ <input id="pw4" type=password>`,
+ returnedFieldIDsByFormLike: [[]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 password fields outside of a <form> (1 empty, 3 full) with skipEmpty",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password value="pass2">
+ <input id="pw3" type=password value="pass3">
+ <input id="pw4" type=password value="pass4">`,
+ returnedFieldIDsByFormLike: [["pw2", "pw3", "pw4"]],
+ skipEmptyFields: true,
+ },
+ {
+ description: "Form with 1 password field",
+ document: `<form><input id="pw1" type=password></form>`,
+ returnedFieldIDsByFormLike: [["pw1"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "Form with 2 password fields",
+ document: `<form><input id="pw1" type=password><input id='pw2' type=password></form>`,
+ returnedFieldIDsByFormLike: [["pw1", "pw2"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field in a form, 1 outside",
+ document: `<form><input id="pw1" type=password></form><input id="pw2" type=password>`,
+ returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form='form1'>
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [[], []],
+ skipEmptyFields: true,
+ },
+ {
+ description: "skipEmptyFields should also skip white-space only fields",
+ document: `<input id="pw-space" type=password value=" ">
+ <input id="pw-tab" type=password value=" ">
+ <input id="pw-newline" type=password form="form1" value="
+">
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [[], []],
+ skipEmptyFields: true,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty with 1 empty",
+ document: `<input id="pw1" type=password value=" pass1 "><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [["pw1"], []],
+ skipEmptyFields: true,
+ },
+];
+
+for (let tc of TESTCASES) {
+ do_print("Sanity checking the testcase: " + tc.description);
+
+ (function() {
+ let testcase = tc;
+ add_task(function*() {
+ do_print("Starting testcase: " + testcase.description);
+ let document = MockDocument.createTestDocument("http://localhost:8080/test/",
+ testcase.document);
+
+ let mapRootElementToFormLike = new Map();
+ for (let input of document.querySelectorAll("input")) {
+ let formLike = LoginFormFactory.createFromField(input);
+ let existingFormLike = mapRootElementToFormLike.get(formLike.rootElement);
+ if (!existingFormLike) {
+ mapRootElementToFormLike.set(formLike.rootElement, formLike);
+ continue;
+ }
+
+ // If the formLike is already present, ensure that the properties are the same.
+ do_print("Checking if the new FormLike for the same root has the same properties");
+ formLikeEqual(formLike, existingFormLike);
+ }
+
+ Assert.strictEqual(mapRootElementToFormLike.size, testcase.returnedFieldIDsByFormLike.length,
+ "Check the correct number of different formLikes were returned");
+
+ let formLikeIndex = -1;
+ for (let formLikeFromInput of mapRootElementToFormLike.values()) {
+ formLikeIndex++;
+ let pwFields = LoginManagerContent._getPasswordFields(formLikeFromInput,
+ testcase.skipEmptyFields);
+
+ if (formLikeFromInput.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
+ let formLikeFromForm = LoginFormFactory.createFromForm(formLikeFromInput.rootElement);
+ do_print("Checking that the FormLike created for the <form> matches" +
+ " the one from a password field");
+ formLikeEqual(formLikeFromInput, formLikeFromForm);
+ }
+
+
+ if (testcase.returnedFieldIDsByFormLike[formLikeIndex].length === 0) {
+ Assert.strictEqual(pwFields, null,
+ "If no password fields were found null should be returned");
+ } else {
+ Assert.strictEqual(pwFields.length,
+ testcase.returnedFieldIDsByFormLike[formLikeIndex].length,
+ "Check the # of password fields for formLike #" + formLikeIndex);
+ }
+
+ for (let i = 0; i < testcase.returnedFieldIDsByFormLike[formLikeIndex].length; i++) {
+ let expectedID = testcase.returnedFieldIDsByFormLike[formLikeIndex][i];
+ Assert.strictEqual(pwFields[i].element.id, expectedID,
+ "Check password field " + i + " ID");
+ }
+ }
+ });
+ })();
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js b/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js
new file mode 100644
index 000000000..f2773ec62
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js
@@ -0,0 +1,28 @@
+/*
+ * Test for LoginUtils._getPasswordOrigin
+ */
+
+"use strict";
+
+const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const TESTCASES = [
+ ["javascript:void(0);", null],
+ ["javascript:void(0);", "javascript:", true],
+ ["chrome://MyAccount", null],
+ ["data:text/html,example", null],
+ ["http://username:password@example.com:80/foo?bar=baz#fragment", "http://example.com", true],
+ ["http://127.0.0.1:80/foo", "http://127.0.0.1"],
+ ["http://[::1]:80/foo", "http://[::1]"],
+ ["http://example.com:8080/foo", "http://example.com:8080"],
+ ["http://127.0.0.1:8080/foo", "http://127.0.0.1:8080", true],
+ ["http://[::1]:8080/foo", "http://[::1]:8080"],
+ ["https://example.com:443/foo", "https://example.com"],
+ ["https://[::1]:443/foo", "https://[::1]"],
+ ["https://[::1]:8443/foo", "https://[::1]:8443"],
+ ["ftp://username:password@[::1]:2121/foo", "ftp://[::1]:2121"],
+];
+
+for (let [input, expected, allowJS] of TESTCASES) {
+ let actual = LMCBackstagePass.LoginUtils._getPasswordOrigin(input, allowJS);
+ Assert.strictEqual(actual, expected, "Checking: " + input);
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js b/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js
new file mode 100644
index 000000000..660910dff
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js
@@ -0,0 +1,40 @@
+/*
+ * Test LoginHelper.isOriginMatching
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+
+add_task(function test_isOriginMatching() {
+ let testcases = [
+ // Index 0 holds the expected return value followed by arguments to isOriginMatching.
+ [true, "http://example.com", "http://example.com"],
+ [true, "http://example.com:8080", "http://example.com:8080"],
+ [true, "https://example.com", "https://example.com"],
+ [true, "https://example.com:8443", "https://example.com:8443"],
+ [false, "http://example.com", "http://mozilla.org"],
+ [false, "http://example.com", "http://example.com:8080"],
+ [false, "https://example.com", "http://example.com"],
+ [false, "https://example.com", "https://mozilla.org"],
+ [false, "http://example.com", "http://sub.example.com"],
+ [false, "https://example.com", "https://sub.example.com"],
+ [false, "http://example.com", "https://example.com:8443"],
+ [false, "http://example.com:8080", "http://example.com:8081"],
+ [false, "http://example.com", ""],
+ [false, "", "http://example.com"],
+ [true, "http://example.com", "https://example.com", { schemeUpgrades: true }],
+ [true, "https://example.com", "https://example.com", { schemeUpgrades: true }],
+ [true, "http://example.com:8080", "http://example.com:8080", { schemeUpgrades: true }],
+ [true, "https://example.com:8443", "https://example.com:8443", { schemeUpgrades: true }],
+ [false, "https://example.com", "http://example.com", { schemeUpgrades: true }], // downgrade
+ [false, "http://example.com:8080", "https://example.com", { schemeUpgrades: true }], // port mismatch
+ [false, "http://example.com", "https://example.com:8443", { schemeUpgrades: true }], // port mismatch
+ [false, "http://sub.example.com", "http://example.com", { schemeUpgrades: true }],
+ ];
+ for (let tc of testcases) {
+ let expected = tc.shift();
+ Assert.strictEqual(LoginHelper.isOriginMatching(...tc), expected,
+ "Check " + JSON.stringify(tc));
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js b/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js
new file mode 100644
index 000000000..4e16aa267
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js
@@ -0,0 +1,107 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the legacy case of a login store containing entries that have an empty
+ * string in the formSubmitURL field.
+ *
+ * In normal conditions, for the purpose of login autocomplete, HTML forms are
+ * identified using both the prePath of the URI on which they are located, and
+ * the prePath of the URI where the data will be submitted. This is represented
+ * by the hostname and formSubmitURL properties of the stored nsILoginInfo.
+ *
+ * When a new login for use in forms is saved (after the user replies to the
+ * password prompt), it is always stored with both the hostname and the
+ * formSubmitURL (that will be equal to the hostname when the form has no
+ * "action" attribute).
+ *
+ * When the same form is displayed again, the password is autocompleted. If
+ * there is another form on the same site that submits to a different site, it
+ * is considered a different form, so the password is not autocompleted, but a
+ * new password can be stored for the other form.
+ *
+ * However, the login database might contain data for an nsILoginInfo that has a
+ * valid hostname, but an empty formSubmitURL. This means that the login
+ * applies to all forms on the site, regardless of where they submit data to.
+ *
+ * A site can have at most one such login, and in case it is present, then it is
+ * not possible to store separate logins for forms on the same site that submit
+ * data to different sites.
+ *
+ * The only way to have such condition is to be using logins that were initially
+ * saved by a very old version of the browser, or because of data manually added
+ * by an extension in an old version.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Adds a login with an empty formSubmitURL, then it verifies that no other
+ * form logins can be added for the same host.
+ */
+add_task(function test_addLogin_wildcard()
+{
+ let loginInfo = TestData.formLogin({ hostname: "http://any.example.com",
+ formSubmitURL: "" });
+ Services.logins.addLogin(loginInfo);
+
+ // Normal form logins cannot be added anymore.
+ loginInfo = TestData.formLogin({ hostname: "http://any.example.com" });
+ Assert.throws(() => Services.logins.addLogin(loginInfo), /already exists/);
+
+ // Authentication logins can still be added.
+ loginInfo = TestData.authLogin({ hostname: "http://any.example.com" });
+ Services.logins.addLogin(loginInfo);
+
+ // Form logins can be added for other hosts.
+ loginInfo = TestData.formLogin({ hostname: "http://other.example.com" });
+ Services.logins.addLogin(loginInfo);
+});
+
+/**
+ * Verifies that findLogins, searchLogins, and countLogins include all logins
+ * that have an empty formSubmitURL in the store, even when a formSubmitURL is
+ * specified.
+ */
+add_task(function test_search_all_wildcard()
+{
+ // Search a given formSubmitURL on any host.
+ let matchData = newPropertyBag({ formSubmitURL: "http://www.example.com" });
+ do_check_eq(Services.logins.searchLogins({}, matchData).length, 2);
+
+ do_check_eq(Services.logins.findLogins({}, "", "http://www.example.com",
+ null).length, 2);
+
+ do_check_eq(Services.logins.countLogins("", "http://www.example.com",
+ null), 2);
+
+ // Restrict the search to one host.
+ matchData.setProperty("hostname", "http://any.example.com");
+ do_check_eq(Services.logins.searchLogins({}, matchData).length, 1);
+
+ do_check_eq(Services.logins.findLogins({}, "http://any.example.com",
+ "http://www.example.com",
+ null).length, 1);
+
+ do_check_eq(Services.logins.countLogins("http://any.example.com",
+ "http://www.example.com",
+ null), 1);
+});
+
+/**
+ * Verifies that specifying an empty string for formSubmitURL in searchLogins
+ * includes only logins that have an empty formSubmitURL in the store.
+ */
+add_task(function test_searchLogins_wildcard()
+{
+ let logins = Services.logins.searchLogins({},
+ newPropertyBag({ formSubmitURL: "" }));
+
+ let loginInfo = TestData.formLogin({ hostname: "http://any.example.com",
+ formSubmitURL: "" });
+ LoginTestUtils.assertLoginListsEqual(logins, [loginInfo]);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js b/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js
new file mode 100644
index 000000000..709bc9818
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the legacy validation made when storing nsILoginInfo or disabled hosts.
+ *
+ * These rules exist because of limitations of the "signons.txt" storage file,
+ * that is not used anymore. They are still enforced by the Login Manager
+ * service, despite these values can now be safely stored in the back-end.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Tests legacy validation with addLogin.
+ */
+add_task(function test_addLogin_invalid_characters_legacy()
+{
+ // Test newlines and carriage returns in properties that contain URLs.
+ for (let testValue of ["http://newline\n.example.com",
+ "http://carriagereturn.example.com\r"]) {
+ let loginInfo = TestData.formLogin({ hostname: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+
+ loginInfo = TestData.formLogin({ formSubmitURL: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+
+ loginInfo = TestData.authLogin({ httpRealm: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+ }
+
+ // Test newlines and carriage returns in form field names.
+ for (let testValue of ["newline_field\n", "carriagereturn\r_field"]) {
+ let loginInfo = TestData.formLogin({ usernameField: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+
+ loginInfo = TestData.formLogin({ passwordField: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+ }
+
+ // Test a single dot as the value of usernameField and formSubmitURL.
+ let loginInfo = TestData.formLogin({ usernameField: "." });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't be periods/);
+
+ loginInfo = TestData.formLogin({ formSubmitURL: "." });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't be periods/);
+
+ // Test the sequence " (" inside the value of the "hostname" property.
+ loginInfo = TestData.formLogin({ hostname: "http://parens (.example.com" });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /bad parens in hostname/);
+});
+
+/**
+ * Tests legacy validation with setLoginSavingEnabled.
+ */
+add_task(function test_setLoginSavingEnabled_invalid_characters_legacy()
+{
+ for (let hostname of ["http://newline\n.example.com",
+ "http://carriagereturn.example.com\r",
+ "."]) {
+ Assert.throws(() => Services.logins.setLoginSavingEnabled(hostname, false),
+ /Invalid hostname/);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_change.js b/toolkit/components/passwordmgr/test/unit/test_logins_change.js
new file mode 100644
index 000000000..79c6d2f54
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_change.js
@@ -0,0 +1,384 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests methods that add, remove, and modify logins.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Verifies that the specified login is considered invalid by addLogin and by
+ * modifyLogin with both nsILoginInfo and nsIPropertyBag arguments.
+ *
+ * This test requires that the login store is empty.
+ *
+ * @param aLoginInfo
+ * nsILoginInfo corresponding to an invalid login.
+ * @param aExpectedError
+ * This argument is passed to the "Assert.throws" test to determine which
+ * error is expected from the modification functions.
+ */
+function checkLoginInvalid(aLoginInfo, aExpectedError)
+{
+ // Try to add the new login, and verify that no data is stored.
+ Assert.throws(() => Services.logins.addLogin(aLoginInfo), aExpectedError);
+ LoginTestUtils.checkLogins([]);
+
+ // Add a login for the modification tests.
+ let testLogin = TestData.formLogin({ hostname: "http://modify.example.com" });
+ Services.logins.addLogin(testLogin);
+
+ // Try to modify the existing login using nsILoginInfo and nsIPropertyBag.
+ Assert.throws(() => Services.logins.modifyLogin(testLogin, aLoginInfo),
+ aExpectedError);
+ Assert.throws(() => Services.logins.modifyLogin(testLogin, newPropertyBag({
+ hostname: aLoginInfo.hostname,
+ formSubmitURL: aLoginInfo.formSubmitURL,
+ httpRealm: aLoginInfo.httpRealm,
+ username: aLoginInfo.username,
+ password: aLoginInfo.password,
+ usernameField: aLoginInfo.usernameField,
+ passwordField: aLoginInfo.passwordField,
+ })), aExpectedError);
+
+ // Verify that no data was stored by the previous calls.
+ LoginTestUtils.checkLogins([testLogin]);
+ Services.logins.removeLogin(testLogin);
+}
+
+/**
+ * Verifies that two objects are not the same instance
+ * but have equal attributes.
+ *
+ * @param {Object} objectA
+ * An object to compare.
+ *
+ * @param {Object} objectB
+ * Another object to compare.
+ *
+ * @param {string[]} attributes
+ * Attributes to compare.
+ *
+ * @return true if all passed attributes are equal for both objects, false otherwise.
+ */
+function compareAttributes(objectA, objectB, attributes) {
+ // If it's the same object, we want to return false.
+ if (objectA == objectB) {
+ return false;
+ }
+ return attributes.every(attr => objectA[attr] == objectB[attr]);
+}
+
+// Tests
+
+/**
+ * Tests that adding logins to the database works.
+ */
+add_task(function test_addLogin_removeLogin()
+{
+ // Each login from the test data should be valid and added to the list.
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.addLogin(loginInfo);
+ }
+ LoginTestUtils.checkLogins(TestData.loginList());
+
+ // Trying to add each login again should result in an error.
+ for (let loginInfo of TestData.loginList()) {
+ Assert.throws(() => Services.logins.addLogin(loginInfo), /already exists/);
+ }
+
+ // Removing each login should succeed.
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.removeLogin(loginInfo);
+ }
+
+ LoginTestUtils.checkLogins([]);
+});
+
+/**
+ * Tests invalid combinations of httpRealm and formSubmitURL.
+ *
+ * For an nsILoginInfo to be valid for storage, one of the two properties should
+ * be strictly equal to null, and the other must not be null or an empty string.
+ *
+ * The legacy case of an empty string in formSubmitURL and a null value in
+ * httpRealm is also supported for storage at the moment.
+ */
+add_task(function test_invalid_httpRealm_formSubmitURL()
+{
+ // httpRealm === null, formSubmitURL === null
+ checkLoginInvalid(TestData.formLogin({ formSubmitURL: null }),
+ /without a httpRealm or formSubmitURL/);
+
+ // httpRealm === "", formSubmitURL === null
+ checkLoginInvalid(TestData.authLogin({ httpRealm: "" }),
+ /without a httpRealm or formSubmitURL/);
+
+ // httpRealm === null, formSubmitURL === ""
+ // This is not enforced for now.
+ // checkLoginInvalid(TestData.formLogin({ formSubmitURL: "" }),
+ // /without a httpRealm or formSubmitURL/);
+
+ // httpRealm === "", formSubmitURL === ""
+ checkLoginInvalid(TestData.formLogin({ formSubmitURL: "", httpRealm: "" }),
+ /both a httpRealm and formSubmitURL/);
+
+ // !!httpRealm, !!formSubmitURL
+ checkLoginInvalid(TestData.formLogin({ httpRealm: "The HTTP Realm" }),
+ /both a httpRealm and formSubmitURL/);
+
+ // httpRealm === "", !!formSubmitURL
+ checkLoginInvalid(TestData.formLogin({ httpRealm: "" }),
+ /both a httpRealm and formSubmitURL/);
+
+ // !!httpRealm, formSubmitURL === ""
+ checkLoginInvalid(TestData.authLogin({ formSubmitURL: "" }),
+ /both a httpRealm and formSubmitURL/);
+});
+
+/**
+ * Tests null or empty values in required login properties.
+ */
+add_task(function test_missing_properties()
+{
+ checkLoginInvalid(TestData.formLogin({ hostname: null }),
+ /null or empty hostname/);
+
+ checkLoginInvalid(TestData.formLogin({ hostname: "" }),
+ /null or empty hostname/);
+
+ checkLoginInvalid(TestData.formLogin({ username: null }),
+ /null username/);
+
+ checkLoginInvalid(TestData.formLogin({ password: null }),
+ /null or empty password/);
+
+ checkLoginInvalid(TestData.formLogin({ password: "" }),
+ /null or empty password/);
+});
+
+/**
+ * Tests invalid NUL characters in nsILoginInfo properties.
+ */
+add_task(function test_invalid_characters()
+{
+ let loginList = [
+ TestData.authLogin({ hostname: "http://null\0X.example.com" }),
+ TestData.authLogin({ httpRealm: "realm\0" }),
+ TestData.formLogin({ formSubmitURL: "http://null\0X.example.com" }),
+ TestData.formLogin({ usernameField: "field\0_null" }),
+ TestData.formLogin({ usernameField: ".\0" }), // Special single dot case
+ TestData.formLogin({ passwordField: "field\0_null" }),
+ TestData.formLogin({ username: "user\0name" }),
+ TestData.formLogin({ password: "pass\0word" }),
+ ];
+ for (let loginInfo of loginList) {
+ checkLoginInvalid(loginInfo, /login values can't contain nulls/);
+ }
+});
+
+/**
+ * Tests removing a login that does not exists.
+ */
+add_task(function test_removeLogin_nonexisting()
+{
+ Assert.throws(() => Services.logins.removeLogin(TestData.formLogin()),
+ /No matching logins/);
+});
+
+/**
+ * Tests removing all logins at once.
+ */
+add_task(function test_removeAllLogins()
+{
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.addLogin(loginInfo);
+ }
+ Services.logins.removeAllLogins();
+ LoginTestUtils.checkLogins([]);
+
+ // The function should also work when there are no logins to delete.
+ Services.logins.removeAllLogins();
+});
+
+/**
+ * Tests the modifyLogin function with an nsILoginInfo argument.
+ */
+add_task(function test_modifyLogin_nsILoginInfo()
+{
+ let loginInfo = TestData.formLogin();
+ let updatedLoginInfo = TestData.formLogin({
+ username: "new username",
+ password: "new password",
+ usernameField: "new_form_field_username",
+ passwordField: "new_form_field_password",
+ });
+ let differentLoginInfo = TestData.authLogin();
+
+ // Trying to modify a login that does not exist should throw.
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, updatedLoginInfo),
+ /No matching logins/);
+
+ // Add the first form login, then modify it to match the second.
+ Services.logins.addLogin(loginInfo);
+ Services.logins.modifyLogin(loginInfo, updatedLoginInfo);
+
+ // The data should now match the second login.
+ LoginTestUtils.checkLogins([updatedLoginInfo]);
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, updatedLoginInfo),
+ /No matching logins/);
+
+ // The login can be changed to have a different type and hostname.
+ Services.logins.modifyLogin(updatedLoginInfo, differentLoginInfo);
+ LoginTestUtils.checkLogins([differentLoginInfo]);
+
+ // It is now possible to add a login with the old type and hostname.
+ Services.logins.addLogin(loginInfo);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ // Modifying a login to match an existing one should not be possible.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, differentLoginInfo),
+ /already exists/);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests the modifyLogin function with an nsIPropertyBag argument.
+ */
+add_task(function test_modifyLogin_nsIProperyBag()
+{
+ let loginInfo = TestData.formLogin();
+ let updatedLoginInfo = TestData.formLogin({
+ username: "new username",
+ password: "new password",
+ usernameField: "",
+ passwordField: "new_form_field_password",
+ });
+ let differentLoginInfo = TestData.authLogin();
+ let differentLoginProperties = newPropertyBag({
+ hostname: differentLoginInfo.hostname,
+ formSubmitURL: differentLoginInfo.formSubmitURL,
+ httpRealm: differentLoginInfo.httpRealm,
+ username: differentLoginInfo.username,
+ password: differentLoginInfo.password,
+ usernameField: differentLoginInfo.usernameField,
+ passwordField: differentLoginInfo.passwordField,
+ });
+
+ // Trying to modify a login that does not exist should throw.
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag()),
+ /No matching logins/);
+
+ // Add the first form login, then modify it to match the second, changing
+ // only some of its properties and checking the behavior with an empty string.
+ Services.logins.addLogin(loginInfo);
+ Services.logins.modifyLogin(loginInfo, newPropertyBag({
+ username: "new username",
+ password: "new password",
+ usernameField: "",
+ passwordField: "new_form_field_password",
+ }));
+
+ // The data should now match the second login.
+ LoginTestUtils.checkLogins([updatedLoginInfo]);
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag()),
+ /No matching logins/);
+
+ // It is also possible to provide no properties to be modified.
+ Services.logins.modifyLogin(updatedLoginInfo, newPropertyBag());
+
+ // Specifying a null property for a required value should throw.
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag({
+ usernameField: null,
+ })));
+
+ // The login can be changed to have a different type and hostname.
+ Services.logins.modifyLogin(updatedLoginInfo, differentLoginProperties);
+ LoginTestUtils.checkLogins([differentLoginInfo]);
+
+ // It is now possible to add a login with the old type and hostname.
+ Services.logins.addLogin(loginInfo);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ // Modifying a login to match an existing one should not be possible.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, differentLoginProperties),
+ /already exists/);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests the login deduplication function.
+ */
+add_task(function test_deduplicate_logins() {
+ // Different key attributes combinations and the amount of unique
+ // results expected for the TestData login list.
+ let keyCombinations = [
+ {
+ keyset: ["username", "password"],
+ results: 13,
+ },
+ {
+ keyset: ["hostname", "username"],
+ results: 17,
+ },
+ {
+ keyset: ["hostname", "username", "password"],
+ results: 18,
+ },
+ {
+ keyset: ["hostname", "username", "password", "formSubmitURL"],
+ results: 23,
+ },
+ ];
+
+ let logins = TestData.loginList();
+
+ for (let testCase of keyCombinations) {
+ // Deduplicate the logins using the current testcase keyset.
+ let deduped = LoginHelper.dedupeLogins(logins, testCase.keyset);
+ Assert.equal(deduped.length, testCase.results, "Correct amount of results.");
+
+ // Checks that every login after deduping is unique.
+ Assert.ok(deduped.every(loginA =>
+ deduped.every(loginB => !compareAttributes(loginA, loginB, testCase.keyset))
+ ), "Every login is unique.");
+ }
+});
+
+/**
+ * Ensure that the login deduplication function keeps the most recent login.
+ */
+add_task(function test_deduplicate_keeps_most_recent() {
+ // Logins to deduplicate.
+ let logins = [
+ TestData.formLogin({timeLastUsed: Date.UTC(2004, 11, 4, 0, 0, 0)}),
+ TestData.formLogin({formSubmitURL: "http://example.com", timeLastUsed: Date.UTC(2015, 11, 4, 0, 0, 0)}),
+ ];
+
+ // Deduplicate the logins.
+ let deduped = LoginHelper.dedupeLogins(logins);
+ Assert.equal(deduped.length, 1, "Deduplicated the logins array.");
+
+ // Verify that the remaining login have the most recent date.
+ let loginTimeLastUsed = deduped[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ Assert.equal(loginTimeLastUsed, Date.UTC(2015, 11, 4, 0, 0, 0), "Most recent login was kept.");
+
+ // Deduplicate the reverse logins array.
+ deduped = LoginHelper.dedupeLogins(logins.reverse());
+ Assert.equal(deduped.length, 1, "Deduplicated the reversed logins array.");
+
+ // Verify that the remaining login have the most recent date.
+ loginTimeLastUsed = deduped[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ Assert.equal(loginTimeLastUsed, Date.UTC(2015, 11, 4, 0, 0, 0), "Most recent login was kept.");
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js b/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js
new file mode 100644
index 000000000..ffbedb4de
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the case where there are logins that cannot be decrypted.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Resets the token used to decrypt logins. This is equivalent to resetting the
+ * master password when it is not known.
+ */
+function resetMasterPassword()
+{
+ let token = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB).getInternalKeyToken();
+ token.reset();
+ token.changePassword("", "");
+}
+
+// Tests
+
+/**
+ * Resets the master password after some logins were added to the database.
+ */
+add_task(function test_logins_decrypt_failure()
+{
+ let logins = TestData.loginList();
+ for (let loginInfo of logins) {
+ Services.logins.addLogin(loginInfo);
+ }
+
+ // This makes the existing logins non-decryptable.
+ resetMasterPassword();
+
+ // These functions don't see the non-decryptable entries anymore.
+ do_check_eq(Services.logins.getAllLogins().length, 0);
+ do_check_eq(Services.logins.findLogins({}, "", "", "").length, 0);
+ do_check_eq(Services.logins.searchLogins({}, newPropertyBag()).length, 0);
+ Assert.throws(() => Services.logins.modifyLogin(logins[0], newPropertyBag()),
+ /No matching logins/);
+ Assert.throws(() => Services.logins.removeLogin(logins[0]),
+ /No matching logins/);
+
+ // The function that counts logins sees the non-decryptable entries also.
+ do_check_eq(Services.logins.countLogins("", "", ""), logins.length);
+
+ // Equivalent logins can be added.
+ for (let loginInfo of logins) {
+ Services.logins.addLogin(loginInfo);
+ }
+ LoginTestUtils.checkLogins(logins);
+ do_check_eq(Services.logins.countLogins("", "", ""), logins.length * 2);
+
+ // Finding logins doesn't return the non-decryptable duplicates.
+ do_check_eq(Services.logins.findLogins({}, "http://www.example.com",
+ "", "").length, 1);
+ let matchData = newPropertyBag({ hostname: "http://www.example.com" });
+ do_check_eq(Services.logins.searchLogins({}, matchData).length, 1);
+
+ // Removing single logins does not remove non-decryptable logins.
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.removeLogin(loginInfo);
+ }
+ do_check_eq(Services.logins.getAllLogins().length, 0);
+ do_check_eq(Services.logins.countLogins("", "", ""), logins.length);
+
+ // Removing all logins removes the non-decryptable entries also.
+ Services.logins.removeAllLogins();
+ do_check_eq(Services.logins.getAllLogins().length, 0);
+ do_check_eq(Services.logins.countLogins("", "", ""), 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js b/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js
new file mode 100644
index 000000000..38344aa7d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js
@@ -0,0 +1,284 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the handling of nsILoginMetaInfo by methods that add, remove, modify,
+ * and find logins.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+var gLooksLikeUUIDRegex = /^\{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\}$/;
+
+/**
+ * Retrieves the only login among the current data that matches the hostname of
+ * the given nsILoginInfo. In case there is more than one login for the
+ * hostname, the test fails.
+ */
+function retrieveLoginMatching(aLoginInfo)
+{
+ let logins = Services.logins.findLogins({}, aLoginInfo.hostname, "", "");
+ do_check_eq(logins.length, 1);
+ return logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+}
+
+/**
+ * Checks that the nsILoginInfo and nsILoginMetaInfo properties of two different
+ * login instances are equal.
+ */
+function assertMetaInfoEqual(aActual, aExpected)
+{
+ do_check_neq(aActual, aExpected);
+
+ // Check the nsILoginInfo properties.
+ do_check_true(aActual.equals(aExpected));
+
+ // Check the nsILoginMetaInfo properties.
+ do_check_eq(aActual.guid, aExpected.guid);
+ do_check_eq(aActual.timeCreated, aExpected.timeCreated);
+ do_check_eq(aActual.timeLastUsed, aExpected.timeLastUsed);
+ do_check_eq(aActual.timePasswordChanged, aExpected.timePasswordChanged);
+ do_check_eq(aActual.timesUsed, aExpected.timesUsed);
+}
+
+/**
+ * nsILoginInfo instances with or without nsILoginMetaInfo properties.
+ */
+var gLoginInfo1;
+var gLoginInfo2;
+var gLoginInfo3;
+
+/**
+ * nsILoginInfo instances reloaded with all the nsILoginMetaInfo properties.
+ * These are often used to provide the reference values to test against.
+ */
+var gLoginMetaInfo1;
+var gLoginMetaInfo2;
+var gLoginMetaInfo3;
+
+// Tests
+
+/**
+ * Prepare the test objects that will be used by the following tests.
+ */
+add_task(function test_initialize()
+{
+ // Use a reference time from ten minutes ago to initialize one instance of
+ // nsILoginMetaInfo, to test that reference times are updated when needed.
+ let baseTimeMs = Date.now() - 600000;
+
+ gLoginInfo1 = TestData.formLogin();
+ gLoginInfo2 = TestData.formLogin({
+ hostname: "http://other.example.com",
+ guid: gUUIDGenerator.generateUUID().toString(),
+ timeCreated: baseTimeMs,
+ timeLastUsed: baseTimeMs + 2,
+ timePasswordChanged: baseTimeMs + 1,
+ timesUsed: 2,
+ });
+ gLoginInfo3 = TestData.authLogin();
+});
+
+/**
+ * Tests the behavior of addLogin with regard to metadata. The logins added
+ * here are also used by the following tests.
+ */
+add_task(function test_addLogin_metainfo()
+{
+ // Add a login without metadata to the database.
+ Services.logins.addLogin(gLoginInfo1);
+
+ // The object provided to addLogin should not have been modified.
+ do_check_eq(gLoginInfo1.guid, null);
+ do_check_eq(gLoginInfo1.timeCreated, 0);
+ do_check_eq(gLoginInfo1.timeLastUsed, 0);
+ do_check_eq(gLoginInfo1.timePasswordChanged, 0);
+ do_check_eq(gLoginInfo1.timesUsed, 0);
+
+ // A login with valid metadata should have been stored.
+ gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1);
+ do_check_true(gLooksLikeUUIDRegex.test(gLoginMetaInfo1.guid));
+ let creationTime = gLoginMetaInfo1.timeCreated;
+ LoginTestUtils.assertTimeIsAboutNow(creationTime);
+ do_check_eq(gLoginMetaInfo1.timeLastUsed, creationTime);
+ do_check_eq(gLoginMetaInfo1.timePasswordChanged, creationTime);
+ do_check_eq(gLoginMetaInfo1.timesUsed, 1);
+
+ // Add a login without metadata to the database.
+ let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo);
+ Services.logins.addLogin(gLoginInfo2);
+
+ // The object provided to addLogin should not have been modified.
+ assertMetaInfoEqual(gLoginInfo2, originalLogin);
+
+ // A login with the provided metadata should have been stored.
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ assertMetaInfoEqual(gLoginMetaInfo2, gLoginInfo2);
+
+ // Add an authentication login to the database before continuing.
+ Services.logins.addLogin(gLoginInfo3);
+ gLoginMetaInfo3 = retrieveLoginMatching(gLoginInfo3);
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+});
+
+/**
+ * Tests that adding a login with a duplicate GUID throws an exception.
+ */
+add_task(function test_addLogin_metainfo_duplicate()
+{
+ let loginInfo = TestData.formLogin({
+ hostname: "http://duplicate.example.com",
+ guid: gLoginMetaInfo2.guid,
+ });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /specified GUID already exists/);
+
+ // Verify that no data was stored by the previous call.
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+});
+
+/**
+ * Tests that the existing metadata is not changed when modifyLogin is called
+ * with an nsILoginInfo argument.
+ */
+add_task(function test_modifyLogin_nsILoginInfo_metainfo_ignored()
+{
+ let newLoginInfo = gLoginInfo1.clone().QueryInterface(Ci.nsILoginMetaInfo);
+ newLoginInfo.guid = gUUIDGenerator.generateUUID().toString();
+ newLoginInfo.timeCreated = Date.now();
+ newLoginInfo.timeLastUsed = Date.now();
+ newLoginInfo.timePasswordChanged = Date.now();
+ newLoginInfo.timesUsed = 12;
+ Services.logins.modifyLogin(gLoginInfo1, newLoginInfo);
+
+ newLoginInfo = retrieveLoginMatching(gLoginInfo1);
+ assertMetaInfoEqual(newLoginInfo, gLoginMetaInfo1);
+});
+
+/**
+ * Tests the modifyLogin function with an nsIProperyBag argument.
+ */
+add_task(function test_modifyLogin_nsIProperyBag_metainfo()
+{
+ // Use a new reference time that is two minutes from now.
+ let newTimeMs = Date.now() + 120000;
+ let newUUIDValue = gUUIDGenerator.generateUUID().toString();
+
+ // Check that properties are changed as requested.
+ Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({
+ guid: newUUIDValue,
+ timeCreated: newTimeMs,
+ timeLastUsed: newTimeMs + 2,
+ timePasswordChanged: newTimeMs + 1,
+ timesUsed: 2,
+ }));
+
+ gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1);
+ do_check_eq(gLoginMetaInfo1.guid, newUUIDValue);
+ do_check_eq(gLoginMetaInfo1.timeCreated, newTimeMs);
+ do_check_eq(gLoginMetaInfo1.timeLastUsed, newTimeMs + 2);
+ do_check_eq(gLoginMetaInfo1.timePasswordChanged, newTimeMs + 1);
+ do_check_eq(gLoginMetaInfo1.timesUsed, 2);
+
+ // Check that timePasswordChanged is updated when changing the password.
+ let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo);
+ Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
+ password: "new password",
+ }));
+ gLoginInfo2.password = "new password";
+
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password);
+ do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
+ do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
+ LoginTestUtils.assertTimeIsAboutNow(gLoginMetaInfo2.timePasswordChanged);
+
+ // Check that timePasswordChanged is not set to the current time when changing
+ // the password and specifying a new value for the property at the same time.
+ Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
+ password: "other password",
+ timePasswordChanged: newTimeMs,
+ }));
+ gLoginInfo2.password = "other password";
+
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password);
+ do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
+ do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
+ do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs);
+
+ // Check the special timesUsedIncrement property.
+ Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
+ timesUsedIncrement: 2,
+ }));
+
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
+ do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
+ do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs);
+ do_check_eq(gLoginMetaInfo2.timesUsed, 4);
+});
+
+/**
+ * Tests that modifying a login to a duplicate GUID throws an exception.
+ */
+add_task(function test_modifyLogin_nsIProperyBag_metainfo_duplicate()
+{
+ Assert.throws(() => Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({
+ guid: gLoginInfo2.guid,
+ })), /specified GUID already exists/);
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+});
+
+/**
+ * Tests searching logins using nsILoginMetaInfo properties.
+ */
+add_task(function test_searchLogins_metainfo()
+{
+ // Find by GUID.
+ let logins = Services.logins.searchLogins({}, newPropertyBag({
+ guid: gLoginMetaInfo1.guid,
+ }));
+ do_check_eq(logins.length, 1);
+ let foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ assertMetaInfoEqual(foundLogin, gLoginMetaInfo1);
+
+ // Find by timestamp.
+ logins = Services.logins.searchLogins({}, newPropertyBag({
+ timePasswordChanged: gLoginMetaInfo2.timePasswordChanged,
+ }));
+ do_check_eq(logins.length, 1);
+ foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ assertMetaInfoEqual(foundLogin, gLoginMetaInfo2);
+
+ // Find using two properties at the same time.
+ logins = Services.logins.searchLogins({}, newPropertyBag({
+ guid: gLoginMetaInfo3.guid,
+ timePasswordChanged: gLoginMetaInfo3.timePasswordChanged,
+ }));
+ do_check_eq(logins.length, 1);
+ foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ assertMetaInfoEqual(foundLogin, gLoginMetaInfo3);
+});
+
+/**
+ * Tests that the default nsILoginManagerStorage module attached to the Login
+ * Manager service is able to save and reload nsILoginMetaInfo properties.
+ */
+add_task(function* test_storage_metainfo()
+{
+ yield* LoginTestUtils.reloadData();
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+
+ assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo1), gLoginMetaInfo1);
+ assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo2), gLoginMetaInfo2);
+ assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo3), gLoginMetaInfo3);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_search.js b/toolkit/components/passwordmgr/test/unit/test_logins_search.js
new file mode 100644
index 000000000..188c75039
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_search.js
@@ -0,0 +1,221 @@
+/*
+ * Tests methods that find specific logins in the store (findLogins,
+ * searchLogins, and countLogins).
+ *
+ * The getAllLogins method is not tested explicitly here, because it is used by
+ * all tests to verify additions, removals and modifications to the login store.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Returns a list of new nsILoginInfo objects that are a subset of the test
+ * data, built to match the specified query.
+ *
+ * @param aQuery
+ * Each property and value of this object restricts the search to those
+ * entries from the test data that match the property exactly.
+ */
+function buildExpectedLogins(aQuery)
+{
+ return TestData.loginList().filter(
+ entry => Object.keys(aQuery).every(name => entry[name] === aQuery[name]));
+}
+
+/**
+ * Tests the searchLogins function.
+ *
+ * @param aQuery
+ * Each property and value of this object is translated to an entry in
+ * the nsIPropertyBag parameter of searchLogins.
+ * @param aExpectedCount
+ * Number of logins from the test data that should be found. The actual
+ * list of logins is obtained using the buildExpectedLogins helper, and
+ * this value is just used to verify that modifications to the test data
+ * don't make the current test meaningless.
+ */
+function checkSearchLogins(aQuery, aExpectedCount)
+{
+ do_print("Testing searchLogins for " + JSON.stringify(aQuery));
+
+ let expectedLogins = buildExpectedLogins(aQuery);
+ do_check_eq(expectedLogins.length, aExpectedCount);
+
+ let outCount = {};
+ let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery));
+ do_check_eq(outCount.value, expectedLogins.length);
+ LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
+}
+
+/**
+ * Tests findLogins, searchLogins, and countLogins with the same query.
+ *
+ * @param aQuery
+ * The "hostname", "formSubmitURL", and "httpRealm" properties of this
+ * object are passed as parameters to findLogins and countLogins. The
+ * same object is then passed to the checkSearchLogins function.
+ * @param aExpectedCount
+ * Number of logins from the test data that should be found. The actual
+ * list of logins is obtained using the buildExpectedLogins helper, and
+ * this value is just used to verify that modifications to the test data
+ * don't make the current test meaningless.
+ */
+function checkAllSearches(aQuery, aExpectedCount)
+{
+ do_print("Testing all search functions for " + JSON.stringify(aQuery));
+
+ let expectedLogins = buildExpectedLogins(aQuery);
+ do_check_eq(expectedLogins.length, aExpectedCount);
+
+ // The findLogins and countLogins functions support wildcard matches by
+ // specifying empty strings as parameters, while searchLogins requires
+ // omitting the property entirely.
+ let hostname = ("hostname" in aQuery) ? aQuery.hostname : "";
+ let formSubmitURL = ("formSubmitURL" in aQuery) ? aQuery.formSubmitURL : "";
+ let httpRealm = ("httpRealm" in aQuery) ? aQuery.httpRealm : "";
+
+ // Test findLogins.
+ let outCount = {};
+ let logins = Services.logins.findLogins(outCount, hostname, formSubmitURL,
+ httpRealm);
+ do_check_eq(outCount.value, expectedLogins.length);
+ LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
+
+ // Test countLogins.
+ let count = Services.logins.countLogins(hostname, formSubmitURL, httpRealm);
+ do_check_eq(count, expectedLogins.length);
+
+ // Test searchLogins.
+ checkSearchLogins(aQuery, aExpectedCount);
+}
+
+// Tests
+
+/**
+ * Prepare data for the following tests.
+ */
+add_task(function test_initialize()
+{
+ for (let login of TestData.loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Tests findLogins, searchLogins, and countLogins with basic queries.
+ */
+add_task(function test_search_all_basic()
+{
+ // Find all logins, using no filters in the search functions.
+ checkAllSearches({}, 23);
+
+ // Find all form logins, then all authentication logins.
+ checkAllSearches({ httpRealm: null }, 14);
+ checkAllSearches({ formSubmitURL: null }, 9);
+
+ // Find all form logins on one host, then all authentication logins.
+ checkAllSearches({ hostname: "http://www4.example.com",
+ httpRealm: null }, 3);
+ checkAllSearches({ hostname: "http://www2.example.org",
+ formSubmitURL: null }, 2);
+
+ // Verify that scheme and subdomain are distinct in the hostname.
+ checkAllSearches({ hostname: "http://www.example.com" }, 1);
+ checkAllSearches({ hostname: "https://www.example.com" }, 1);
+ checkAllSearches({ hostname: "https://example.com" }, 1);
+ checkAllSearches({ hostname: "http://www3.example.com" }, 3);
+
+ // Verify that scheme and subdomain are distinct in formSubmitURL.
+ checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2);
+ checkAllSearches({ formSubmitURL: "https://www.example.com" }, 2);
+ checkAllSearches({ formSubmitURL: "http://example.com" }, 1);
+
+ // Find by formSubmitURL on a single host.
+ checkAllSearches({ hostname: "http://www3.example.com",
+ formSubmitURL: "http://www.example.com" }, 1);
+ checkAllSearches({ hostname: "http://www3.example.com",
+ formSubmitURL: "https://www.example.com" }, 1);
+ checkAllSearches({ hostname: "http://www3.example.com",
+ formSubmitURL: "http://example.com" }, 1);
+
+ // Find by httpRealm on all hosts.
+ checkAllSearches({ httpRealm: "The HTTP Realm" }, 3);
+ checkAllSearches({ httpRealm: "ftp://ftp.example.org" }, 1);
+ checkAllSearches({ httpRealm: "The HTTP Realm Other" }, 2);
+
+ // Find by httpRealm on a single host.
+ checkAllSearches({ hostname: "http://example.net",
+ httpRealm: "The HTTP Realm" }, 1);
+ checkAllSearches({ hostname: "http://example.net",
+ httpRealm: "The HTTP Realm Other" }, 1);
+ checkAllSearches({ hostname: "ftp://example.net",
+ httpRealm: "ftp://example.net" }, 1);
+});
+
+/**
+ * Tests searchLogins with advanced queries.
+ */
+add_task(function test_searchLogins()
+{
+ checkSearchLogins({ usernameField: "form_field_username" }, 12);
+ checkSearchLogins({ passwordField: "form_field_password" }, 13);
+
+ // Find all logins with an empty usernameField, including for authentication.
+ checkSearchLogins({ usernameField: "" }, 11);
+
+ // Find form logins with an empty usernameField.
+ checkSearchLogins({ httpRealm: null,
+ usernameField: "" }, 2);
+
+ // Find logins with an empty usernameField on one host.
+ checkSearchLogins({ hostname: "http://www6.example.com",
+ usernameField: "" }, 1);
+});
+
+/**
+ * Tests searchLogins with invalid arguments.
+ */
+add_task(function test_searchLogins_invalid()
+{
+ Assert.throws(() => Services.logins.searchLogins({},
+ newPropertyBag({ username: "value" })),
+ /Unexpected field/);
+});
+
+/**
+ * Tests that matches are case-sensitive, compare the full field value, and are
+ * strict when interpreting the prePath of URIs.
+ */
+add_task(function test_search_all_full_case_sensitive()
+{
+ checkAllSearches({ hostname: "http://www.example.com" }, 1);
+ checkAllSearches({ hostname: "http://www.example.com/" }, 0);
+ checkAllSearches({ hostname: "http://" }, 0);
+ checkAllSearches({ hostname: "example.com" }, 0);
+
+ checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2);
+ checkAllSearches({ formSubmitURL: "http://www.example.com/" }, 0);
+ checkAllSearches({ formSubmitURL: "http://" }, 0);
+ checkAllSearches({ formSubmitURL: "example.com" }, 0);
+
+ checkAllSearches({ httpRealm: "The HTTP Realm" }, 3);
+ checkAllSearches({ httpRealm: "The http Realm" }, 0);
+ checkAllSearches({ httpRealm: "The HTTP" }, 0);
+ checkAllSearches({ httpRealm: "Realm" }, 0);
+});
+
+/**
+ * Tests findLogins, searchLogins, and countLogins with queries that should
+ * return no values.
+ */
+add_task(function test_search_all_empty()
+{
+ checkAllSearches({ hostname: "http://nonexistent.example.com" }, 0);
+ checkAllSearches({ formSubmitURL: "http://www.example.com",
+ httpRealm: "The HTTP Realm" }, 0);
+
+ checkSearchLogins({ hostname: "" }, 0);
+ checkSearchLogins({ id: "1000" }, 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js b/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js
new file mode 100644
index 000000000..19175df59
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js
@@ -0,0 +1,169 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+
+const HOST1 = "https://www.example.com/";
+const HOST2 = "https://www.mozilla.org/";
+
+const USER1 = "myuser";
+const USER2 = "anotheruser";
+
+const PASS1 = "mypass";
+const PASS2 = "anotherpass";
+const PASS3 = "yetanotherpass";
+
+add_task(function test_new_logins() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST2,
+ formSubmitURL: HOST2,
+ });
+
+ Assert.ok(importedLogin, "Return value should indicate another imported login.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST2});
+ Assert.equal(matchingLogins.length, 1, `There should also be 1 login for ${HOST2}`);
+ Assert.equal(Services.logins.getAllLogins().length, 2, "There should be 2 logins in total");
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_duplicate_logins() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(!importedLogin, "Return value should indicate no new login was imported.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_different_passwords() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ timeCreated: new Date(Date.now() - 1000),
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ // This item will be newer, so its password should take precedence.
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS2,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ timeCreated: new Date(),
+ });
+ Assert.ok(!importedLogin, "Return value should not indicate imported login (as we updated an existing one).");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Assert.equal(matchingLogins[0].password, PASS2, "We should have updated the password for this login.");
+
+ // Now try to update with an older password:
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS3,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ timeCreated: new Date(Date.now() - 1000000),
+ });
+ Assert.ok(!importedLogin, "Return value should not indicate imported login (as we didn't update anything).");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Assert.equal(matchingLogins[0].password, PASS2, "We should NOT have updated the password for this login.");
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_different_usernames() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER2,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate another imported login.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 2, `There should now be 2 logins for ${HOST1}`);
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_different_targets() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ // Not passing either a formSubmitURL or a httpRealm should be treated as
+ // the same as the previous login
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ });
+ Assert.ok(!importedLogin, "Return value should NOT indicate imported login " +
+ "(because a missing formSubmitURL and httpRealm should be duped to the existing login).");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Assert.equal(matchingLogins[0].formSubmitURL, HOST1, "The form submission URL should have been kept.");
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ httpRealm: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate another imported login " +
+ "as an httpRealm login shouldn't be duped.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 2, `There should now be 2 logins for ${HOST1}`);
+
+ Services.logins.removeAllLogins();
+});
+
diff --git a/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js b/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js
new file mode 100644
index 000000000..b8793e1bd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js
@@ -0,0 +1,243 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the LoginImport object.
+ */
+
+"use strict";
+
+// Globals
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
+ "resource://gre/modules/LoginImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
+ "resource://gre/modules/LoginStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gLoginManagerCrypto",
+ "@mozilla.org/login-manager/crypto/SDR;1",
+ "nsILoginManagerCrypto");
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+/**
+ * Creates empty login data tables in the given SQLite connection, resembling
+ * the most recent schema version (excluding indices).
+ */
+function promiseCreateDatabaseSchema(aConnection)
+{
+ return Task.spawn(function* () {
+ yield aConnection.setSchemaVersion(5);
+ yield aConnection.execute("CREATE TABLE moz_logins (" +
+ "id INTEGER PRIMARY KEY," +
+ "hostname TEXT NOT NULL," +
+ "httpRealm TEXT," +
+ "formSubmitURL TEXT," +
+ "usernameField TEXT NOT NULL," +
+ "passwordField TEXT NOT NULL," +
+ "encryptedUsername TEXT NOT NULL," +
+ "encryptedPassword TEXT NOT NULL," +
+ "guid TEXT," +
+ "encType INTEGER," +
+ "timeCreated INTEGER," +
+ "timeLastUsed INTEGER," +
+ "timePasswordChanged INTEGER," +
+ "timesUsed INTEGER)");
+ yield aConnection.execute("CREATE TABLE moz_disabledHosts (" +
+ "id INTEGER PRIMARY KEY," +
+ "hostname TEXT UNIQUE)");
+ yield aConnection.execute("CREATE TABLE moz_deleted_logins (" +
+ "id INTEGER PRIMARY KEY," +
+ "guid TEXT," +
+ "timeDeleted INTEGER)");
+ });
+}
+
+/**
+ * Inserts a new entry in the database resembling the given nsILoginInfo object.
+ */
+function promiseInsertLoginInfo(aConnection, aLoginInfo)
+{
+ aLoginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // We can't use the aLoginInfo object directly in the execute statement
+ // because the bind code in Sqlite.jsm doesn't allow objects with extra
+ // properties beyond those being binded. So we might as well use an array as
+ // it is simpler.
+ let values = [
+ aLoginInfo.hostname,
+ aLoginInfo.httpRealm,
+ aLoginInfo.formSubmitURL,
+ aLoginInfo.usernameField,
+ aLoginInfo.passwordField,
+ gLoginManagerCrypto.encrypt(aLoginInfo.username),
+ gLoginManagerCrypto.encrypt(aLoginInfo.password),
+ aLoginInfo.guid,
+ aLoginInfo.encType,
+ aLoginInfo.timeCreated,
+ aLoginInfo.timeLastUsed,
+ aLoginInfo.timePasswordChanged,
+ aLoginInfo.timesUsed,
+ ];
+
+ return aConnection.execute("INSERT INTO moz_logins (hostname, " +
+ "httpRealm, formSubmitURL, usernameField, " +
+ "passwordField, encryptedUsername, " +
+ "encryptedPassword, guid, encType, timeCreated, " +
+ "timeLastUsed, timePasswordChanged, timesUsed) " +
+ "VALUES (?" + ",?".repeat(12) + ")", values);
+}
+
+/**
+ * Inserts a new disabled host entry in the database.
+ */
+function promiseInsertDisabledHost(aConnection, aHostname)
+{
+ return aConnection.execute("INSERT INTO moz_disabledHosts (hostname) " +
+ "VALUES (?)", [aHostname]);
+}
+
+// Tests
+
+/**
+ * Imports login data from a SQLite file constructed using the test data.
+ */
+add_task(function* test_import()
+{
+ let store = new LoginStore(getTempFile("test-import.json").path);
+ let loginsSqlite = getTempFile("test-logins.sqlite").path;
+
+ // Prepare the logins to be imported, including the nsILoginMetaInfo data.
+ let loginList = TestData.loginList();
+ for (let loginInfo of loginList) {
+ loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+ loginInfo.guid = gUUIDGenerator.generateUUID().toString();
+ loginInfo.timeCreated = Date.now();
+ loginInfo.timeLastUsed = Date.now();
+ loginInfo.timePasswordChanged = Date.now();
+ loginInfo.timesUsed = 1;
+ }
+
+ // Create and populate the SQLite database first.
+ let connection = yield Sqlite.openConnection({ path: loginsSqlite });
+ try {
+ yield promiseCreateDatabaseSchema(connection);
+ for (let loginInfo of loginList) {
+ yield promiseInsertLoginInfo(connection, loginInfo);
+ }
+ yield promiseInsertDisabledHost(connection, "http://www.example.com");
+ yield promiseInsertDisabledHost(connection, "https://www.example.org");
+ } finally {
+ yield connection.close();
+ }
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ yield new LoginImport(store, loginsSqlite).import();
+
+ // Verify that every login in the test data has a matching imported row.
+ do_check_eq(loginList.length, store.data.logins.length);
+ do_check_true(loginList.every(function (loginInfo) {
+ return store.data.logins.some(function (loginDataItem) {
+ let username = gLoginManagerCrypto.decrypt(loginDataItem.encryptedUsername);
+ let password = gLoginManagerCrypto.decrypt(loginDataItem.encryptedPassword);
+ return loginDataItem.hostname == loginInfo.hostname &&
+ loginDataItem.httpRealm == loginInfo.httpRealm &&
+ loginDataItem.formSubmitURL == loginInfo.formSubmitURL &&
+ loginDataItem.usernameField == loginInfo.usernameField &&
+ loginDataItem.passwordField == loginInfo.passwordField &&
+ username == loginInfo.username &&
+ password == loginInfo.password &&
+ loginDataItem.guid == loginInfo.guid &&
+ loginDataItem.encType == loginInfo.encType &&
+ loginDataItem.timeCreated == loginInfo.timeCreated &&
+ loginDataItem.timeLastUsed == loginInfo.timeLastUsed &&
+ loginDataItem.timePasswordChanged == loginInfo.timePasswordChanged &&
+ loginDataItem.timesUsed == loginInfo.timesUsed;
+ });
+ }));
+
+ // Verify that disabled hosts have been imported.
+ do_check_eq(store.data.disabledHosts.length, 2);
+ do_check_true(store.data.disabledHosts.indexOf("http://www.example.com") != -1);
+ do_check_true(store.data.disabledHosts.indexOf("https://www.example.org") != -1);
+});
+
+/**
+ * Tests imports of NULL values due to a downgraded database.
+ */
+add_task(function* test_import_downgraded()
+{
+ let store = new LoginStore(getTempFile("test-import-downgraded.json").path);
+ let loginsSqlite = getTempFile("test-logins-downgraded.sqlite").path;
+
+ // Create and populate the SQLite database first.
+ let connection = yield Sqlite.openConnection({ path: loginsSqlite });
+ try {
+ yield promiseCreateDatabaseSchema(connection);
+ yield connection.setSchemaVersion(3);
+ yield promiseInsertLoginInfo(connection, TestData.formLogin({
+ guid: gUUIDGenerator.generateUUID().toString(),
+ timeCreated: null,
+ timeLastUsed: null,
+ timePasswordChanged: null,
+ timesUsed: 0,
+ }));
+ } finally {
+ yield connection.close();
+ }
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ yield new LoginImport(store, loginsSqlite).import();
+
+ // Verify that the missing metadata was generated correctly.
+ let loginItem = store.data.logins[0];
+ let creationTime = loginItem.timeCreated;
+ LoginTestUtils.assertTimeIsAboutNow(creationTime);
+ do_check_eq(loginItem.timeLastUsed, creationTime);
+ do_check_eq(loginItem.timePasswordChanged, creationTime);
+ do_check_eq(loginItem.timesUsed, 1);
+});
+
+/**
+ * Verifies that importing from a SQLite file with database version 2 fails.
+ */
+add_task(function* test_import_v2()
+{
+ let store = new LoginStore(getTempFile("test-import-v2.json").path);
+ let loginsSqlite = do_get_file("data/signons-v2.sqlite").path;
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ try {
+ yield new LoginImport(store, loginsSqlite).import();
+ do_throw("The operation should have failed.");
+ } catch (ex) { }
+});
+
+/**
+ * Imports login data from a SQLite file, with database version 3.
+ */
+add_task(function* test_import_v3()
+{
+ let store = new LoginStore(getTempFile("test-import-v3.json").path);
+ let loginsSqlite = do_get_file("data/signons-v3.sqlite").path;
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ yield new LoginImport(store, loginsSqlite).import();
+
+ // We only execute basic integrity checks.
+ do_check_eq(store.data.logins[0].usernameField, "u1");
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js b/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js
new file mode 100644
index 000000000..335eb601b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js
@@ -0,0 +1,206 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the LoginStore object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
+ "resource://gre/modules/LoginStore.jsm");
+
+const TEST_STORE_FILE_NAME = "test-logins.json";
+
+// Tests
+
+/**
+ * Saves login data to a file, then reloads it.
+ */
+add_task(function* test_save_reload()
+{
+ let storeForSave = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ // The "load" method must be called before preparing the data to be saved.
+ yield storeForSave.load();
+
+ let rawLoginData = {
+ id: storeForSave.data.nextId++,
+ hostname: "http://www.example.com",
+ httpRealm: null,
+ formSubmitURL: "http://www.example.com/submit-url",
+ usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
+ passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
+ encryptedUsername: "(test)",
+ encryptedPassword: "(test)",
+ guid: "(test)",
+ encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
+ timeCreated: Date.now(),
+ timeLastUsed: Date.now(),
+ timePasswordChanged: Date.now(),
+ timesUsed: 1,
+ };
+ storeForSave.data.logins.push(rawLoginData);
+
+ storeForSave.data.disabledHosts.push("http://www.example.org");
+
+ yield storeForSave._save();
+
+ // Test the asynchronous initialization path.
+ let storeForLoad = new LoginStore(storeForSave.path);
+ yield storeForLoad.load();
+
+ do_check_eq(storeForLoad.data.logins.length, 1);
+ do_check_matches(storeForLoad.data.logins[0], rawLoginData);
+ do_check_eq(storeForLoad.data.disabledHosts.length, 1);
+ do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
+
+ // Test the synchronous initialization path.
+ storeForLoad = new LoginStore(storeForSave.path);
+ storeForLoad.ensureDataReady();
+
+ do_check_eq(storeForLoad.data.logins.length, 1);
+ do_check_matches(storeForLoad.data.logins[0], rawLoginData);
+ do_check_eq(storeForLoad.data.disabledHosts.length, 1);
+ do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
+});
+
+/**
+ * Checks that loading from a missing file results in empty arrays.
+ */
+add_task(function* test_load_empty()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ yield store.load();
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ do_check_eq(store.data.logins.length, 0);
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
+
+/**
+ * Checks that saving empty data still overwrites any existing file.
+ */
+add_task(function* test_save_empty()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ yield store.load();
+
+ let createdFile = yield OS.File.open(store.path, { create: true });
+ yield createdFile.close();
+
+ yield store._save();
+
+ do_check_true(yield OS.File.exists(store.path));
+});
+
+/**
+ * Loads data from a string in a predefined format. The purpose of this test is
+ * to verify that the JSON format used in previous versions can be loaded.
+ */
+add_task(function* test_load_string_predefined()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ let string = "{\"logins\":[{" +
+ "\"id\":1," +
+ "\"hostname\":\"http://www.example.com\"," +
+ "\"httpRealm\":null," +
+ "\"formSubmitURL\":\"http://www.example.com/submit-url\"," +
+ "\"usernameField\":\"usernameField\"," +
+ "\"passwordField\":\"passwordField\"," +
+ "\"encryptedUsername\":\"(test)\"," +
+ "\"encryptedPassword\":\"(test)\"," +
+ "\"guid\":\"(test)\"," +
+ "\"encType\":1," +
+ "\"timeCreated\":1262304000000," +
+ "\"timeLastUsed\":1262390400000," +
+ "\"timePasswordChanged\":1262476800000," +
+ "\"timesUsed\":1}],\"disabledHosts\":[" +
+ "\"http://www.example.org\"]}";
+
+ yield OS.File.writeAtomic(store.path,
+ new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ do_check_eq(store.data.logins.length, 1);
+ do_check_matches(store.data.logins[0], {
+ id: 1,
+ hostname: "http://www.example.com",
+ httpRealm: null,
+ formSubmitURL: "http://www.example.com/submit-url",
+ usernameField: "usernameField",
+ passwordField: "passwordField",
+ encryptedUsername: "(test)",
+ encryptedPassword: "(test)",
+ guid: "(test)",
+ encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
+ timeCreated: 1262304000000,
+ timeLastUsed: 1262390400000,
+ timePasswordChanged: 1262476800000,
+ timesUsed: 1,
+ });
+
+ do_check_eq(store.data.disabledHosts.length, 1);
+ do_check_eq(store.data.disabledHosts[0], "http://www.example.org");
+});
+
+/**
+ * Loads login data from a malformed JSON string.
+ */
+add_task(function* test_load_string_malformed()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
+ "\"id\":1,";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ // A backup file should have been created.
+ do_check_true(yield OS.File.exists(store.path + ".corrupt"));
+ yield OS.File.remove(store.path + ".corrupt");
+
+ // The store should be ready to accept new data.
+ do_check_eq(store.data.logins.length, 0);
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
+
+/**
+ * Loads login data from a malformed JSON string, using the synchronous
+ * initialization path.
+ */
+add_task(function* test_load_string_malformed_sync()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
+ "\"id\":1,";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ store.ensureDataReady();
+
+ // A backup file should have been created.
+ do_check_true(yield OS.File.exists(store.path + ".corrupt"));
+ yield OS.File.remove(store.path + ".corrupt");
+
+ // The store should be ready to accept new data.
+ do_check_eq(store.data.logins.length, 0);
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_notifications.js b/toolkit/components/passwordmgr/test/unit/test_notifications.js
new file mode 100644
index 000000000..41caa2c1b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_notifications.js
@@ -0,0 +1,172 @@
+/*
+ * Tests notifications dispatched when modifying stored logins.
+ */
+
+var expectedNotification;
+var expectedData;
+
+var TestObserver = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe : function (subject, topic, data) {
+ do_check_eq(topic, "passwordmgr-storage-changed");
+ do_check_eq(data, expectedNotification);
+
+ switch (data) {
+ case "addLogin":
+ do_check_true(subject instanceof Ci.nsILoginInfo);
+ do_check_true(subject instanceof Ci.nsILoginMetaInfo);
+ do_check_true(expectedData.equals(subject)); // nsILoginInfo.equals()
+ break;
+ case "modifyLogin":
+ do_check_true(subject instanceof Ci.nsIArray);
+ do_check_eq(subject.length, 2);
+ var oldLogin = subject.queryElementAt(0, Ci.nsILoginInfo);
+ var newLogin = subject.queryElementAt(1, Ci.nsILoginInfo);
+ do_check_true(expectedData[0].equals(oldLogin)); // nsILoginInfo.equals()
+ do_check_true(expectedData[1].equals(newLogin));
+ break;
+ case "removeLogin":
+ do_check_true(subject instanceof Ci.nsILoginInfo);
+ do_check_true(subject instanceof Ci.nsILoginMetaInfo);
+ do_check_true(expectedData.equals(subject)); // nsILoginInfo.equals()
+ break;
+ case "removeAllLogins":
+ do_check_eq(subject, null);
+ break;
+ case "hostSavingEnabled":
+ case "hostSavingDisabled":
+ do_check_true(subject instanceof Ci.nsISupportsString);
+ do_check_eq(subject.data, expectedData);
+ break;
+ default:
+ do_throw("Unhandled notification: " + data + " / " + topic);
+ }
+
+ expectedNotification = null; // ensure a duplicate is flagged as unexpected.
+ expectedData = null;
+ }
+};
+
+add_task(function test_notifications()
+{
+
+try {
+
+var testnum = 0;
+var testdesc = "Setup of nsLoginInfo test-users";
+
+var testuser1 = new LoginInfo("http://testhost1", "", null,
+ "dummydude", "itsasecret", "put_user_here", "put_pw_here");
+
+var testuser2 = new LoginInfo("http://testhost2", "", null,
+ "dummydude2", "itsasecret2", "put_user2_here", "put_pw2_here");
+
+Services.obs.addObserver(TestObserver, "passwordmgr-storage-changed", false);
+
+
+/* ========== 1 ========== */
+testnum = 1;
+testdesc = "Initial connection to storage module";
+
+/* ========== 2 ========== */
+testnum++;
+testdesc = "addLogin";
+
+expectedNotification = "addLogin";
+expectedData = testuser1;
+Services.logins.addLogin(testuser1);
+LoginTestUtils.checkLogins([testuser1]);
+do_check_eq(expectedNotification, null); // check that observer got a notification
+
+/* ========== 3 ========== */
+testnum++;
+testdesc = "modifyLogin";
+
+expectedNotification = "modifyLogin";
+expectedData = [testuser1, testuser2];
+Services.logins.modifyLogin(testuser1, testuser2);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([testuser2]);
+
+/* ========== 4 ========== */
+testnum++;
+testdesc = "removeLogin";
+
+expectedNotification = "removeLogin";
+expectedData = testuser2;
+Services.logins.removeLogin(testuser2);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 5 ========== */
+testnum++;
+testdesc = "removeAllLogins";
+
+expectedNotification = "removeAllLogins";
+expectedData = null;
+Services.logins.removeAllLogins();
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 6 ========== */
+testnum++;
+testdesc = "removeAllLogins (again)";
+
+expectedNotification = "removeAllLogins";
+expectedData = null;
+Services.logins.removeAllLogins();
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 7 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / false";
+
+expectedNotification = "hostSavingDisabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", false);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ ["http://site.com"]);
+
+/* ========== 8 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / false (again)";
+
+expectedNotification = "hostSavingDisabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", false);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ ["http://site.com"]);
+
+/* ========== 9 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / true";
+
+expectedNotification = "hostSavingEnabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", true);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 10 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / true (again)";
+
+expectedNotification = "hostSavingEnabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", true);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+Services.obs.removeObserver(TestObserver, "passwordmgr-storage-changed");
+
+LoginTestUtils.clearData();
+
+} catch (e) {
+ throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e);
+}
+
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_recipes_add.js b/toolkit/components/passwordmgr/test/unit/test_recipes_add.js
new file mode 100644
index 000000000..ef5086c3b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_recipes_add.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests adding and retrieving LoginRecipes in the parent process.
+ */
+
+"use strict";
+
+add_task(function* test_init() {
+ let parent = new LoginRecipesParent({ defaults: null });
+ let initPromise1 = parent.initializationPromise;
+ let initPromise2 = parent.initializationPromise;
+ Assert.strictEqual(initPromise1, initPromise2, "Check that the same promise is returned");
+
+ let recipesParent = yield initPromise1;
+ Assert.ok(recipesParent instanceof LoginRecipesParent, "Check init return value");
+ Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes");
+});
+
+add_task(function* test_get_missing_host() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ let exampleRecipes = recipesParent.getRecipesForHost("example.invalid");
+ Assert.strictEqual(exampleRecipes.size, 0, "Check recipe count for example.invalid");
+
+});
+
+add_task(function* test_add_get_simple_host() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes");
+ recipesParent.add({
+ hosts: ["example.com"],
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
+});
+
+add_task(function* test_add_get_non_standard_port_host() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com:8080"],
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com:8080");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com:8080");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com:8080", "Check the one host");
+});
+
+add_task(function* test_add_multiple_hosts() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com", "foo.invalid"],
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 2,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 2, "Check that two hosts are present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the first host");
+ Assert.strictEqual(recipe.hosts[1], "foo.invalid", "Check the second host");
+
+ let fooRecipes = recipesParent.getRecipesForHost("foo.invalid");
+ Assert.strictEqual(fooRecipes.size, 1, "Check recipe count for foo.invalid");
+ let fooRecipe = [...fooRecipes][0];
+ Assert.strictEqual(fooRecipe, recipe, "Check that the recipe is shared");
+ Assert.strictEqual(typeof(fooRecipe), "object", "Check recipe type");
+ Assert.strictEqual(fooRecipe.hosts.length, 2, "Check that two hosts are present");
+ Assert.strictEqual(fooRecipe.hosts[0], "example.com", "Check the first host");
+ Assert.strictEqual(fooRecipe.hosts[1], "foo.invalid", "Check the second host");
+});
+
+add_task(function* test_add_pathRegex() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com"],
+ pathRegex: /^\/mypath\//,
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
+ Assert.strictEqual(recipe.pathRegex.toString(), "/^\\/mypath\\//", "Check the pathRegex");
+});
+
+add_task(function* test_add_selectors() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com"],
+ usernameSelector: "#my-username",
+ passwordSelector: "#my-form > input.password",
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
+ Assert.strictEqual(recipe.usernameSelector, "#my-username", "Check the usernameSelector");
+ Assert.strictEqual(recipe.passwordSelector, "#my-form > input.password", "Check the passwordSelector");
+});
+
+/* Begin checking errors with add */
+
+add_task(function* test_add_missing_prop() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({}), /required/, "Some properties are required");
+});
+
+add_task(function* test_add_unknown_prop() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ unknownProp: true,
+ }), /supported/, "Unknown properties should cause an error to help with typos");
+});
+
+add_task(function* test_add_invalid_hosts() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: 404,
+ }), /array/, "hosts should be an array");
+});
+
+add_task(function* test_add_empty_host_array() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: [],
+ }), /array/, "hosts should be a non-empty array");
+});
+
+add_task(function* test_add_pathRegex_non_regexp() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: ["example.com"],
+ pathRegex: "foo",
+ }), /regular expression/, "pathRegex should be a RegExp");
+});
+
+add_task(function* test_add_usernameSelector_non_string() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: ["example.com"],
+ usernameSelector: 404,
+ }), /string/, "usernameSelector should be a string");
+});
+
+add_task(function* test_add_passwordSelector_non_string() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: ["example.com"],
+ passwordSelector: 404,
+ }), /string/, "passwordSelector should be a string");
+});
+
+/* End checking errors with add */
diff --git a/toolkit/components/passwordmgr/test/unit/test_recipes_content.js b/toolkit/components/passwordmgr/test/unit/test_recipes_content.js
new file mode 100644
index 000000000..3d3751452
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_recipes_content.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test filtering recipes in LoginRecipesContent.
+ */
+
+"use strict";
+
+Cu.importGlobalProperties(["URL"]);
+
+add_task(function* test_getFieldOverrides() {
+ let recipes = new Set([
+ { // path doesn't match but otherwise good
+ hosts: ["example.com:8080"],
+ passwordSelector: "#password",
+ pathRegex: /^\/$/,
+ usernameSelector: ".username",
+ },
+ { // match with no field overrides
+ hosts: ["example.com:8080"],
+ },
+ { // best match (field selectors + path match)
+ description: "best match",
+ hosts: ["a.invalid", "example.com:8080", "other.invalid"],
+ passwordSelector: "#password",
+ pathRegex: /^\/first\/second\/$/,
+ usernameSelector: ".username",
+ },
+ ]);
+
+ let form = MockDocument.createTestDocument("http://localhost:8080/first/second/", "<form>").
+ forms[0];
+ let override = LoginRecipesContent.getFieldOverrides(recipes, form);
+ Assert.strictEqual(override.description, "best match",
+ "Check the best field override recipe was returned");
+ Assert.strictEqual(override.usernameSelector, ".username", "Check usernameSelector");
+ Assert.strictEqual(override.passwordSelector, "#password", "Check passwordSelector");
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js b/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js
new file mode 100644
index 000000000..51a107170
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js
@@ -0,0 +1,69 @@
+/**
+ * Tests the LoginHelper object.
+ */
+
+"use strict";
+
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+
+function* createSignonFile(singon) {
+ let {file, pref} = singon;
+
+ if (pref) {
+ Services.prefs.setCharPref(pref, file);
+ }
+
+ yield OS.File.writeAtomic(
+ OS.Path.join(OS.Constants.Path.profileDir, file), new Uint8Array(1));
+}
+
+function* isSignonClear(singon) {
+ const {file, pref} = singon;
+ const fileExists = yield OS.File.exists(
+ OS.Path.join(OS.Constants.Path.profileDir, file));
+
+ if (pref) {
+ try {
+ Services.prefs.getCharPref(pref);
+ return false;
+ } catch (e) {}
+ }
+
+ return !fileExists;
+}
+
+add_task(function* test_remove_lagecy_signonfile() {
+ // In the last test case, signons3.txt being deleted even when
+ // it doesn't exist.
+ const signonsSettings = [[
+ { file: "signons.txt" },
+ { file: "signons2.txt" },
+ { file: "signons3.txt" }
+ ], [
+ { file: "signons.txt", pref: "signon.SignonFileName" },
+ { file: "signons2.txt", pref: "signon.SignonFileName2" },
+ { file: "signons3.txt", pref: "signon.SignonFileName3" }
+ ], [
+ { file: "signons2.txt" },
+ { file: "singons.txt", pref: "signon.SignonFileName" },
+ { file: "customized2.txt", pref: "signon.SignonFileName2" },
+ { file: "customized3.txt", pref: "signon.SignonFileName3" }
+ ]];
+
+ for (let setting of signonsSettings) {
+ for (let singon of setting) {
+ yield createSignonFile(singon);
+ }
+
+ LoginHelper.removeLegacySignonFiles();
+
+ for (let singon of setting) {
+ equal(yield isSignonClear(singon), true);
+ }
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js b/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js
new file mode 100644
index 000000000..3406becff
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js
@@ -0,0 +1,184 @@
+/*
+ * Test Services.logins.searchLogins with the `schemeUpgrades` property.
+ */
+
+const HTTP3_ORIGIN = "http://www3.example.com";
+const HTTPS_ORIGIN = "https://www.example.com";
+const HTTP_ORIGIN = "http://www.example.com";
+
+/**
+ * Returns a list of new nsILoginInfo objects that are a subset of the test
+ * data, built to match the specified query.
+ *
+ * @param {Object} aQuery
+ * Each property and value of this object restricts the search to those
+ * entries from the test data that match the property exactly.
+ */
+function buildExpectedLogins(aQuery) {
+ return TestData.loginList().filter(
+ entry => Object.keys(aQuery).every(name => {
+ if (name == "schemeUpgrades") {
+ return true;
+ }
+ if (["hostname", "formSubmitURL"].includes(name)) {
+ return LoginHelper.isOriginMatching(entry[name], aQuery[name], {
+ schemeUpgrades: aQuery.schemeUpgrades,
+ });
+ }
+ return entry[name] === aQuery[name];
+ }));
+}
+
+/**
+ * Tests the searchLogins function.
+ *
+ * @param {Object} aQuery
+ * Each property and value of this object is translated to an entry in
+ * the nsIPropertyBag parameter of searchLogins.
+ * @param {Number} aExpectedCount
+ * Number of logins from the test data that should be found. The actual
+ * list of logins is obtained using the buildExpectedLogins helper, and
+ * this value is just used to verify that modifications to the test data
+ * don't make the current test meaningless.
+ */
+function checkSearch(aQuery, aExpectedCount) {
+ do_print("Testing searchLogins for " + JSON.stringify(aQuery));
+
+ let expectedLogins = buildExpectedLogins(aQuery);
+ do_check_eq(expectedLogins.length, aExpectedCount);
+
+ let outCount = {};
+ let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery));
+ do_check_eq(outCount.value, expectedLogins.length);
+ LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
+}
+
+/**
+ * Prepare data for the following tests.
+ */
+add_task(function test_initialize() {
+ for (let login of TestData.loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Tests searchLogins with the `schemeUpgrades` property
+ */
+add_task(function test_search_schemeUpgrades_hostname() {
+ // Hostname-only
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 2);
+});
+
+/**
+ * Same as above but replacing hostname with formSubmitURL.
+ */
+add_task(function test_search_schemeUpgrades_formSubmitURL() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 4);
+});
+
+
+add_task(function test_search_schemeUpgrades_hostname_formSubmitURL() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ passwordField: "form_field_password",
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ httpRealm: null,
+ passwordField: "form_field_password",
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+});
+
+/**
+ * HTTP submitting to HTTPS
+ */
+add_task(function test_http_to_https() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTP3_ORIGIN,
+ httpRealm: null,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTP3_ORIGIN,
+ httpRealm: null,
+ schemeUpgrades: true,
+ }, 2);
+});
+
+/**
+ * schemeUpgrades shouldn't cause downgrades
+ */
+add_task(function test_search_schemeUpgrades_downgrade() {
+ checkSearch({
+ formSubmitURL: HTTP_ORIGIN,
+ hostname: HTTP_ORIGIN,
+ }, 1);
+ do_print("The same number should be found with schemeUpgrades since we're searching for HTTP");
+ checkSearch({
+ formSubmitURL: HTTP_ORIGIN,
+ hostname: HTTP_ORIGIN,
+ schemeUpgrades: true,
+ }, 1);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_storage.js b/toolkit/components/passwordmgr/test/unit/test_storage.js
new file mode 100644
index 000000000..d65516d9b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_storage.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the default nsILoginManagerStorage module attached to the Login
+ * Manager service is able to save and reload nsILoginInfo properties correctly,
+ * even when they include special characters.
+ */
+
+"use strict";
+
+// Globals
+
+function* reloadAndCheckLoginsGen(aExpectedLogins)
+{
+ yield LoginTestUtils.reloadData();
+ LoginTestUtils.checkLogins(aExpectedLogins);
+ LoginTestUtils.clearData();
+}
+
+// Tests
+
+/**
+ * Tests addLogin with valid non-ASCII characters.
+ */
+add_task(function* test_storage_addLogin_nonascii()
+{
+ let hostname = "http://" + String.fromCharCode(355) + ".example.com";
+
+ // Store the strings "user" and "pass" using similarly looking glyphs.
+ let loginInfo = TestData.formLogin({
+ hostname: hostname,
+ formSubmitURL: hostname,
+ username: String.fromCharCode(533, 537, 7570, 345),
+ password: String.fromCharCode(421, 259, 349, 537),
+ usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
+ passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
+ });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+
+ // Store the string "test" using similarly looking glyphs.
+ loginInfo = TestData.authLogin({
+ httpRealm: String.fromCharCode(355, 277, 349, 357),
+ });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+});
+
+/**
+ * Tests addLogin with newline characters in the username and password.
+ */
+add_task(function* test_storage_addLogin_newlines()
+{
+ let loginInfo = TestData.formLogin({
+ username: "user\r\nname",
+ password: "password\r\n",
+ });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+});
+
+/**
+ * Tests addLogin with a single dot in fields where it is allowed.
+ *
+ * These tests exist to verify the legacy "signons.txt" storage format.
+ */
+add_task(function* test_storage_addLogin_dot()
+{
+ let loginInfo = TestData.formLogin({ hostname: ".", passwordField: "." });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+
+ loginInfo = TestData.authLogin({ httpRealm: "." });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+});
+
+/**
+ * Tests addLogin with parentheses in hostnames.
+ *
+ * These tests exist to verify the legacy "signons.txt" storage format.
+ */
+add_task(function* test_storage_addLogin_parentheses()
+{
+ let loginList = [
+ TestData.authLogin({ httpRealm: "(realm" }),
+ TestData.authLogin({ httpRealm: "realm)" }),
+ TestData.authLogin({ httpRealm: "(realm)" }),
+ TestData.authLogin({ httpRealm: ")realm(" }),
+ TestData.authLogin({ hostname: "http://parens(.example.com" }),
+ TestData.authLogin({ hostname: "http://parens).example.com" }),
+ TestData.authLogin({ hostname: "http://parens(example).example.com" }),
+ TestData.authLogin({ hostname: "http://parens)example(.example.com" }),
+ ];
+ for (let loginInfo of loginList) {
+ Services.logins.addLogin(loginInfo);
+ }
+ yield* reloadAndCheckLoginsGen(loginList);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js
new file mode 100644
index 000000000..8eab6efe5
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js
@@ -0,0 +1,507 @@
+/*
+ * This test interfaces directly with the mozStorage password storage module,
+ * bypassing the normal password manager usage.
+ */
+
+
+const ENCTYPE_BASE64 = 0;
+const ENCTYPE_SDR = 1;
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+// Current schema version used by storage-mozStorage.js. This will need to be
+// kept in sync with the version there (or else the tests fail).
+const CURRENT_SCHEMA = 6;
+
+function* copyFile(aLeafName)
+{
+ yield OS.File.copy(OS.Path.join(do_get_file("data").path, aLeafName),
+ OS.Path.join(OS.Constants.Path.profileDir, aLeafName));
+}
+
+function openDB(aLeafName)
+{
+ var dbFile = new FileUtils.File(OS.Constants.Path.profileDir);
+ dbFile.append(aLeafName);
+
+ return Services.storage.openDatabase(dbFile);
+}
+
+function deleteFile(pathname, filename)
+{
+ var file = new FileUtils.File(pathname);
+ file.append(filename);
+
+ // Suppress failures, this happens in the mozstorage tests on Windows
+ // because the module may still be holding onto the DB. (We don't
+ // have a way to explicitly shutdown/GC the module).
+ try {
+ if (file.exists())
+ file.remove(false);
+ } catch (e) {}
+}
+
+function reloadStorage(aInputPathName, aInputFileName)
+{
+ var inputFile = null;
+ if (aInputFileName) {
+ inputFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ inputFile.initWithPath(aInputPathName);
+ inputFile.append(aInputFileName);
+ }
+
+ let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"]
+ .createInstance(Ci.nsILoginManagerStorage);
+ storage.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIVariant)
+ .initWithFile(inputFile);
+
+ return storage;
+}
+
+function checkStorageData(storage, ref_disabledHosts, ref_logins)
+{
+ LoginTestUtils.assertLoginListsEqual(storage.getAllLogins(), ref_logins);
+ LoginTestUtils.assertDisabledHostsEqual(getAllDisabledHostsFromPermissionManager(),
+ ref_disabledHosts);
+}
+
+function getAllDisabledHostsFromPermissionManager() {
+ let disabledHosts = [];
+ let enumerator = Services.perms.enumerator;
+
+ while (enumerator.hasMoreElements()) {
+ let perm = enumerator.getNext();
+ if (perm.type == PERMISSION_SAVE_LOGINS && perm.capability == Services.perms.DENY_ACTION) {
+ disabledHosts.push(perm.principal.URI.prePath);
+ }
+ }
+
+ return disabledHosts;
+}
+
+function setLoginSavingEnabled(origin, enabled) {
+ let uri = Services.io.newURI(origin, null, null);
+
+ if (enabled) {
+ Services.perms.remove(uri, PERMISSION_SAVE_LOGINS);
+ } else {
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ }
+}
+
+add_task(function* test_execute()
+{
+
+const OUTDIR = OS.Constants.Path.profileDir;
+
+try {
+
+var isGUID = /^\{[0-9a-f\d]{8}-[0-9a-f\d]{4}-[0-9a-f\d]{4}-[0-9a-f\d]{4}-[0-9a-f\d]{12}\}$/;
+function getGUIDforID(conn, id) {
+ var stmt = conn.createStatement("SELECT guid from moz_logins WHERE id = " + id);
+ stmt.executeStep();
+ var guid = stmt.getString(0);
+ stmt.finalize();
+ return guid;
+}
+
+function getEncTypeForID(conn, id) {
+ var stmt = conn.createStatement("SELECT encType from moz_logins WHERE id = " + id);
+ stmt.executeStep();
+ var encType = stmt.row.encType;
+ stmt.finalize();
+ return encType;
+}
+
+function getAllDisabledHostsFromMozStorage(conn) {
+ let disabledHosts = [];
+ let stmt = conn.createStatement("SELECT hostname from moz_disabledHosts");
+
+ while (stmt.executeStep()) {
+ disabledHosts.push(stmt.row.hostname);
+ }
+
+ return disabledHosts;
+}
+
+var storage;
+var dbConnection;
+var testnum = 0;
+var testdesc = "Setup of nsLoginInfo test-users";
+var nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Components.interfaces.nsILoginInfo);
+do_check_true(nsLoginInfo != null);
+
+var testuser1 = new nsLoginInfo;
+testuser1.init("http://test.com", "http://test.com", null,
+ "testuser1", "testpass1", "u1", "p1");
+var testuser1B = new nsLoginInfo;
+testuser1B.init("http://test.com", "http://test.com", null,
+ "testuser1B", "testpass1B", "u1", "p1");
+var testuser2 = new nsLoginInfo;
+testuser2.init("http://test.org", "http://test.org", null,
+ "testuser2", "testpass2", "u2", "p2");
+var testuser3 = new nsLoginInfo;
+testuser3.init("http://test.gov", "http://test.gov", null,
+ "testuser3", "testpass3", "u3", "p3");
+var testuser4 = new nsLoginInfo;
+testuser4.init("http://test.gov", "http://test.gov", null,
+ "testuser1", "testpass2", "u4", "p4");
+var testuser5 = new nsLoginInfo;
+testuser5.init("http://test.gov", "http://test.gov", null,
+ "testuser2", "testpass1", "u5", "p5");
+
+
+/* ========== 1 ========== */
+testnum++;
+testdesc = "Test downgrade from v999 storage";
+
+yield* copyFile("signons-v999.sqlite");
+// Verify the schema version in the test file.
+dbConnection = openDB("signons-v999.sqlite");
+do_check_eq(999, dbConnection.schemaVersion);
+dbConnection.close();
+
+storage = reloadStorage(OUTDIR, "signons-v999.sqlite");
+setLoginSavingEnabled("https://disabled.net", false);
+checkStorageData(storage, ["https://disabled.net"], [testuser1]);
+
+// Check to make sure we downgraded the schema version.
+dbConnection = openDB("signons-v999.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v999.sqlite");
+
+/* ========== 2 ========== */
+testnum++;
+testdesc = "Test downgrade from incompat v999 storage";
+// This file has a testuser999/testpass999, but is missing an expected column
+
+var origFile = OS.Path.join(OUTDIR, "signons-v999-2.sqlite");
+var failFile = OS.Path.join(OUTDIR, "signons-v999-2.sqlite.corrupt");
+
+// Make sure we always start clean in a clean state.
+yield* copyFile("signons-v999-2.sqlite");
+yield OS.File.remove(failFile);
+
+Assert.throws(() => reloadStorage(OUTDIR, "signons-v999-2.sqlite"),
+ /Initialization failed/);
+
+// Check to ensure the DB file was renamed to .corrupt.
+do_check_false(yield OS.File.exists(origFile));
+do_check_true(yield OS.File.exists(failFile));
+
+yield OS.File.remove(failFile);
+
+/* ========== 3 ========== */
+testnum++;
+testdesc = "Test upgrade from v1->v2 storage";
+
+yield* copyFile("signons-v1.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v1.sqlite");
+do_check_eq(1, dbConnection.schemaVersion);
+dbConnection.close();
+
+storage = reloadStorage(OUTDIR, "signons-v1.sqlite");
+checkStorageData(storage, ["https://disabled.net"], [testuser1, testuser2]);
+
+// Check to see that we added a GUIDs to the logins.
+dbConnection = openDB("signons-v1.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+var guid = getGUIDforID(dbConnection, 1);
+do_check_true(isGUID.test(guid));
+guid = getGUIDforID(dbConnection, 2);
+do_check_true(isGUID.test(guid));
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v1.sqlite");
+
+/* ========== 4 ========== */
+testnum++;
+testdesc = "Test upgrade v2->v1 storage";
+// This is the case where a v2 DB has been accessed with v1 code, and now we
+// are upgrading it again. Any logins added by the v1 code must be properly
+// upgraded.
+
+yield* copyFile("signons-v1v2.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v1v2.sqlite");
+do_check_eq(1, dbConnection.schemaVersion);
+dbConnection.close();
+
+storage = reloadStorage(OUTDIR, "signons-v1v2.sqlite");
+checkStorageData(storage, ["https://disabled.net"], [testuser1, testuser2, testuser3]);
+
+// While we're here, try modifying a login, to ensure that doing so doesn't
+// change the existing GUID.
+storage.modifyLogin(testuser1, testuser1B);
+checkStorageData(storage, ["https://disabled.net"], [testuser1B, testuser2, testuser3]);
+
+// Check the GUIDs. Logins 1 and 2 should retain their original GUID, login 3
+// should have one created (because it didn't have one previously).
+dbConnection = openDB("signons-v1v2.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+guid = getGUIDforID(dbConnection, 1);
+do_check_eq("{655c7358-f1d6-6446-adab-53f98ac5d80f}", guid);
+guid = getGUIDforID(dbConnection, 2);
+do_check_eq("{13d9bfdc-572a-4d4e-9436-68e9803e84c1}", guid);
+guid = getGUIDforID(dbConnection, 3);
+do_check_true(isGUID.test(guid));
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v1v2.sqlite");
+
+/* ========== 5 ========== */
+testnum++;
+testdesc = "Test upgrade from v2->v3 storage";
+
+yield* copyFile("signons-v2.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v2.sqlite");
+do_check_eq(2, dbConnection.schemaVersion);
+
+storage = reloadStorage(OUTDIR, "signons-v2.sqlite");
+
+// Check to see that we added the correct encType to the logins.
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+var encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+dbConnection.close();
+
+// There are 4 logins, but 3 will be invalid because we can no longer decrypt
+// base64-encoded items. (testuser1/4/5)
+checkStorageData(storage, ["https://disabled.net"],
+ [testuser2]);
+
+deleteFile(OUTDIR, "signons-v2.sqlite");
+
+/* ========== 6 ========== */
+testnum++;
+testdesc = "Test upgrade v3->v2 storage";
+// This is the case where a v3 DB has been accessed with v2 code, and now we
+// are upgrading it again. Any logins added by the v2 code must be properly
+// upgraded.
+
+yield* copyFile("signons-v2v3.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v2v3.sqlite");
+do_check_eq(2, dbConnection.schemaVersion);
+encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, null];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+
+// Reload storage, check that the new login now has encType=1, others untouched
+storage = reloadStorage(OUTDIR, "signons-v2v3.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, ENCTYPE_SDR];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+
+// Sanity check that the data gets migrated
+// There are 5 logins, but 3 will be invalid because we can no longer decrypt
+// base64-encoded items. (testuser1/4/5). We no longer reencrypt with SDR.
+checkStorageData(storage, ["https://disabled.net"], [testuser2, testuser3]);
+encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, ENCTYPE_SDR];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v2v3.sqlite");
+
+
+/* ========== 7 ========== */
+testnum++;
+testdesc = "Test upgrade from v3->v4 storage";
+
+yield* copyFile("signons-v3.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v3.sqlite");
+do_check_eq(3, dbConnection.schemaVersion);
+
+storage = reloadStorage(OUTDIR, "signons-v3.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+// Remove old entry from permission manager.
+setLoginSavingEnabled("https://disabled.net", true);
+
+// Check that timestamps and counts were initialized correctly
+checkStorageData(storage, [], [testuser1, testuser2]);
+
+var logins = storage.getAllLogins();
+for (var i = 0; i < 2; i++) {
+ do_check_true(logins[i] instanceof Ci.nsILoginMetaInfo);
+ do_check_eq(1, logins[i].timesUsed);
+ LoginTestUtils.assertTimeIsAboutNow(logins[i].timeCreated);
+ LoginTestUtils.assertTimeIsAboutNow(logins[i].timeLastUsed);
+ LoginTestUtils.assertTimeIsAboutNow(logins[i].timePasswordChanged);
+}
+
+/* ========== 8 ========== */
+testnum++;
+testdesc = "Test upgrade from v3->v4->v3 storage";
+
+yield* copyFile("signons-v3v4.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v3v4.sqlite");
+do_check_eq(3, dbConnection.schemaVersion);
+
+storage = reloadStorage(OUTDIR, "signons-v3v4.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+// testuser1 already has timestamps, testuser2 does not.
+checkStorageData(storage, [], [testuser1, testuser2]);
+
+logins = storage.getAllLogins();
+
+var t1, t2;
+if (logins[0].username == "testuser1") {
+ t1 = logins[0];
+ t2 = logins[1];
+} else {
+ t1 = logins[1];
+ t2 = logins[0];
+}
+
+do_check_true(t1 instanceof Ci.nsILoginMetaInfo);
+do_check_true(t2 instanceof Ci.nsILoginMetaInfo);
+
+do_check_eq(9, t1.timesUsed);
+do_check_eq(1262049951275, t1.timeCreated);
+do_check_eq(1262049951275, t1.timeLastUsed);
+do_check_eq(1262049951275, t1.timePasswordChanged);
+
+do_check_eq(1, t2.timesUsed);
+LoginTestUtils.assertTimeIsAboutNow(t2.timeCreated);
+LoginTestUtils.assertTimeIsAboutNow(t2.timeLastUsed);
+LoginTestUtils.assertTimeIsAboutNow(t2.timePasswordChanged);
+
+
+/* ========== 9 ========== */
+testnum++;
+testdesc = "Test upgrade from v4 storage";
+
+yield* copyFile("signons-v4.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v4.sqlite");
+do_check_eq(4, dbConnection.schemaVersion);
+do_check_false(dbConnection.tableExists("moz_deleted_logins"));
+
+storage = reloadStorage(OUTDIR, "signons-v4.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_deleted_logins"));
+
+
+/* ========== 10 ========== */
+testnum++;
+testdesc = "Test upgrade from v4->v5->v4 storage";
+
+yield copyFile("signons-v4v5.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v4v5.sqlite");
+do_check_eq(4, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_deleted_logins"));
+
+storage = reloadStorage(OUTDIR, "signons-v4v5.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_deleted_logins"));
+
+/* ========== 11 ========== */
+testnum++;
+testdesc = "Test upgrade from v5->v6 storage";
+
+yield* copyFile("signons-v5v6.sqlite");
+
+// Sanity check the test file.
+dbConnection = openDB("signons-v5v6.sqlite");
+do_check_eq(5, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_disabledHosts"));
+
+// Initial disabled hosts inside signons-v5v6.sqlite
+var disabledHosts = [
+ "http://disabled1.example.com",
+ "http://大.net",
+ "http://xn--19g.com"
+];
+
+LoginTestUtils.assertDisabledHostsEqual(disabledHosts, getAllDisabledHostsFromMozStorage(dbConnection));
+
+// Reload storage
+storage = reloadStorage(OUTDIR, "signons-v5v6.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+// moz_disabledHosts should now be empty after migration.
+LoginTestUtils.assertDisabledHostsEqual([], getAllDisabledHostsFromMozStorage(dbConnection));
+
+// Get all the other hosts currently saved in the permission manager.
+let hostsInPermissionManager = getAllDisabledHostsFromPermissionManager();
+
+// All disabledHosts should have migrated to the permission manager
+LoginTestUtils.assertDisabledHostsEqual(disabledHosts, hostsInPermissionManager);
+
+// Remove all disabled hosts from the permission manager before test ends
+for (let host of disabledHosts) {
+ setLoginSavingEnabled(host, true);
+}
+
+/* ========== 12 ========== */
+testnum++;
+testdesc = "Create nsILoginInfo instances for testing with";
+
+testuser1 = new nsLoginInfo;
+testuser1.init("http://dummyhost.mozilla.org", "", null,
+ "dummydude", "itsasecret", "put_user_here", "put_pw_here");
+
+
+/*
+ * ---------------------- DB Corruption ----------------------
+ * Try to initialize with a corrupt database file. This should create a backup
+ * file, then upon next use create a new database file.
+ */
+
+/* ========== 13 ========== */
+testnum++;
+testdesc = "Corrupt database and backup";
+
+const filename = "signons-c.sqlite";
+const filepath = OS.Path.join(OS.Constants.Path.profileDir, filename);
+
+yield OS.File.copy(do_get_file("data/corruptDB.sqlite").path, filepath);
+
+// will init mozStorage module with corrupt database, init should fail
+Assert.throws(
+ () => reloadStorage(OS.Constants.Path.profileDir, filename),
+ /Initialization failed/);
+
+// check that the backup file exists
+do_check_true(yield OS.File.exists(filepath + ".corrupt"));
+
+// check that the original corrupt file has been deleted
+do_check_false(yield OS.File.exists(filepath));
+
+// initialize the storage module again
+storage = reloadStorage(OS.Constants.Path.profileDir, filename);
+
+// use the storage module again, should work now
+storage.addLogin(testuser1);
+checkStorageData(storage, [], [testuser1]);
+
+// check the file exists
+var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+file.initWithPath(OS.Constants.Path.profileDir);
+file.append(filename);
+do_check_true(file.exists());
+
+deleteFile(OS.Constants.Path.profileDir, filename + ".corrupt");
+deleteFile(OS.Constants.Path.profileDir, filename);
+
+} catch (e) {
+ throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e);
+}
+
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_telemetry.js b/toolkit/components/passwordmgr/test/unit/test_telemetry.js
new file mode 100644
index 000000000..1d8f80226
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_telemetry.js
@@ -0,0 +1,187 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the statistics and other counters reported through telemetry.
+ */
+
+"use strict";
+
+// Globals
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+// To prevent intermittent failures when the test is executed at a time that is
+// very close to a day boundary, we make it deterministic by using a static
+// reference date for all the time-based statistics.
+const gReferenceTimeMs = new Date("2000-01-01T00:00:00").getTime();
+
+// Returns a milliseconds value to use with nsILoginMetaInfo properties, falling
+// approximately in the middle of the specified number of days before the
+// reference time, where zero days indicates a time within the past 24 hours.
+var daysBeforeMs = days => gReferenceTimeMs - (days + 0.5) * MS_PER_DAY;
+
+/**
+ * Contains metadata that will be attached to test logins in order to verify
+ * that the statistics collection is working properly. Most properties of the
+ * logins are initialized to the default test values already.
+ *
+ * If you update this data or any of the telemetry histograms it checks, you'll
+ * probably need to update the expected statistics in the test below.
+ */
+const StatisticsTestData = [
+ {
+ timeLastUsed: daysBeforeMs(0),
+ },
+ {
+ timeLastUsed: daysBeforeMs(1),
+ },
+ {
+ timeLastUsed: daysBeforeMs(7),
+ formSubmitURL: null,
+ httpRealm: "The HTTP Realm",
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(7),
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(30),
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(31),
+ },
+ {
+ timeLastUsed: daysBeforeMs(365),
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(366),
+ },
+ {
+ // If the login was saved in the future, it is ignored for statistiscs.
+ timeLastUsed: daysBeforeMs(-1),
+ },
+ {
+ timeLastUsed: daysBeforeMs(1000),
+ },
+];
+
+/**
+ * Triggers the collection of those statistics that are not accumulated each
+ * time an action is taken, but are a static snapshot of the current state.
+ */
+function triggerStatisticsCollection() {
+ Services.obs.notifyObservers(null, "gather-telemetry", "" + gReferenceTimeMs);
+}
+
+/**
+ * Tests the telemetry histogram with the given ID contains only the specified
+ * non-zero ranges, expressed in the format { range1: value1, range2: value2 }.
+ */
+function testHistogram(histogramId, expectedNonZeroRanges) {
+ let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
+
+ // Compute the actual ranges in the format { range1: value1, range2: value2 }.
+ let actualNonZeroRanges = {};
+ for (let [index, range] of snapshot.ranges.entries()) {
+ let value = snapshot.counts[index];
+ if (value > 0) {
+ actualNonZeroRanges[range] = value;
+ }
+ }
+
+ // These are stringified to visualize the differences between the values.
+ do_print("Testing histogram: " + histogramId);
+ do_check_eq(JSON.stringify(actualNonZeroRanges),
+ JSON.stringify(expectedNonZeroRanges));
+}
+
+// Tests
+
+/**
+ * Enable local telemetry recording for the duration of the tests, and prepare
+ * the test data that will be used by the following tests.
+ */
+add_task(function test_initialize() {
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ do_register_cleanup(function () {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+
+ let uniqueNumber = 1;
+ for (let loginModifications of StatisticsTestData) {
+ loginModifications.hostname = `http://${uniqueNumber++}.example.com`;
+ Services.logins.addLogin(TestData.formLogin(loginModifications));
+ }
+});
+
+/**
+ * Tests the collection of statistics related to login metadata.
+ */
+add_task(function test_logins_statistics() {
+ // Repeat the operation twice to test that histograms are not accumulated.
+ for (let repeating of [false, true]) {
+ triggerStatisticsCollection();
+
+ // Should record 1 in the bucket corresponding to the number of passwords.
+ testHistogram("PWMGR_NUM_SAVED_PASSWORDS",
+ { 10: 1 });
+
+ // Should record 1 in the bucket corresponding to the number of passwords.
+ testHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS",
+ { 1: 1 });
+
+ // For each saved login, should record 1 in the bucket corresponding to the
+ // age in days since the login was last used.
+ testHistogram("PWMGR_LOGIN_LAST_USED_DAYS",
+ { 0: 1, 1: 1, 7: 2, 29: 2, 356: 2, 750: 1 });
+
+ // Should record the number of logins without a username in bucket 0, and
+ // the number of logins with a username in bucket 1.
+ testHistogram("PWMGR_USERNAME_PRESENT",
+ { 0: 4, 1: 6 });
+ }
+});
+
+/**
+ * Tests the collection of statistics related to hosts for which passowrd saving
+ * has been explicitly disabled.
+ */
+add_task(function test_disabledHosts_statistics() {
+ // Should record 1 in the bucket corresponding to the number of sites for
+ // which password saving is disabled.
+ Services.logins.setLoginSavingEnabled("http://www.example.com", false);
+ triggerStatisticsCollection();
+ testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 1: 1 });
+
+ Services.logins.setLoginSavingEnabled("http://www.example.com", true);
+ triggerStatisticsCollection();
+ testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 0: 1 });
+});
+
+/**
+ * Tests the collection of statistics related to general settings.
+ */
+add_task(function test_settings_statistics() {
+ let oldRememberSignons = Services.prefs.getBoolPref("signon.rememberSignons");
+ do_register_cleanup(function () {
+ Services.prefs.setBoolPref("signon.rememberSignons", oldRememberSignons);
+ });
+
+ // Repeat the operation twice per value to test that histograms are reset.
+ for (let remember of [false, true, false, true]) {
+ // This change should be observed immediately by the login service.
+ Services.prefs.setBoolPref("signon.rememberSignons", remember);
+
+ triggerStatisticsCollection();
+
+ // Should record 1 in either bucket 0 or bucket 1 based on the preference.
+ testHistogram("PWMGR_SAVING_ENABLED", remember ? { 1: 1 } : { 0: 1 });
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js b/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js
new file mode 100644
index 000000000..e1d250a76
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js
@@ -0,0 +1,488 @@
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+const PREF_INSECURE_FIELD_WARNING_ENABLED = "security.insecure_field_warning.contextual.enabled";
+const PREF_INSECURE_AUTOFILLFORMS_ENABLED = "signon.autofillForms.http";
+
+let matchingLogins = [];
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "emptypass1", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword"));
+
+let meta = matchingLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+const LABEL_NO_USERNAME = "No username (" + time + ")";
+
+let expectedResults = [
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: []
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: []
+ },
+];
+
+add_task(function* test_all_patterns() {
+ LoginHelper.createLogger("UserAutoCompleteResult");
+ expectedResults.forEach(pattern => {
+ Services.prefs.setBoolPref(PREF_INSECURE_FIELD_WARNING_ENABLED,
+ pattern.insecureFieldWarningEnabled);
+ Services.prefs.setBoolPref(PREF_INSECURE_AUTOFILLFORMS_ENABLED,
+ pattern.insecureAutoFillFormsEnabled);
+ let actual = new UserAutoCompleteResult("", pattern.matchingLogins,
+ {
+ isSecure: pattern.isSecure,
+ isPasswordField: pattern.isPasswordField
+ });
+ pattern.items.forEach((item, index) => {
+ equal(actual.getValueAt(index), item.value);
+ equal(actual.getLabelAt(index), item.label);
+ equal(actual.getStyleAt(index), item.style);
+ });
+
+ if (pattern.items.length != 0) {
+ Assert.throws(() => actual.getValueAt(pattern.items.length),
+ /Index out of range\./);
+
+ Assert.throws(() => actual.getLabelAt(pattern.items.length),
+ /Index out of range\./);
+
+ Assert.throws(() => actual.removeValueAt(pattern.items.length, true),
+ /Index out of range\./);
+ }
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/unit/xpcshell.ini b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
new file mode 100644
index 000000000..8f8c92a28
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -0,0 +1,46 @@
+[DEFAULT]
+head = head.js
+tail =
+support-files = data/**
+
+# Test JSON file access and import from SQLite, not applicable to Android.
+[test_module_LoginImport.js]
+skip-if = os == "android"
+[test_module_LoginStore.js]
+skip-if = os == "android"
+[test_removeLegacySignonFiles.js]
+skip-if = os == "android"
+
+# Test SQLite database backup and migration, applicable to Android only.
+[test_storage_mozStorage.js]
+skip-if = true || os != "android" # Bug 1171687: Needs fixing on Android
+
+# The following tests apply to any storage back-end.
+[test_context_menu.js]
+skip-if = os == "android" # The context menu isn't used on Android.
+# LoginManagerContextMenu is only included for MOZ_BUILD_APP == 'browser'.
+run-if = buildapp == "browser"
+[test_dedupeLogins.js]
+[test_disabled_hosts.js]
+[test_getFormFields.js]
+[test_getPasswordFields.js]
+[test_getPasswordOrigin.js]
+[test_isOriginMatching.js]
+[test_legacy_empty_formSubmitURL.js]
+[test_legacy_validation.js]
+[test_logins_change.js]
+[test_logins_decrypt_failure.js]
+skip-if = os == "android" # Bug 1171687: Needs fixing on Android
+[test_user_autocomplete_result.js]
+skip-if = os == "android"
+[test_logins_metainfo.js]
+[test_logins_search.js]
+[test_maybeImportLogin.js]
+[test_notifications.js]
+[test_OSCrypto_win.js]
+skip-if = os != "win"
+[test_recipes_add.js]
+[test_recipes_content.js]
+[test_search_schemeUpgrades.js]
+[test_storage.js]
+[test_telemetry.js]