Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | this.EXPORTED_SYMBOLS = ["LoginManagerContent"]; |
michael@0 | 6 | |
michael@0 | 7 | const Ci = Components.interfaces; |
michael@0 | 8 | const Cr = Components.results; |
michael@0 | 9 | const Cc = Components.classes; |
michael@0 | 10 | const Cu = Components.utils; |
michael@0 | 11 | |
michael@0 | 12 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 13 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 14 | Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | // These mirror signon.* prefs. |
michael@0 | 17 | var gEnabled, gDebug, gAutofillForms, gStoreWhenAutocompleteOff; |
michael@0 | 18 | |
michael@0 | 19 | function log(...pieces) { |
michael@0 | 20 | function generateLogMessage(args) { |
michael@0 | 21 | let strings = ['Login Manager (content):']; |
michael@0 | 22 | |
michael@0 | 23 | args.forEach(function(arg) { |
michael@0 | 24 | if (typeof arg === 'string') { |
michael@0 | 25 | strings.push(arg); |
michael@0 | 26 | } else if (typeof arg === 'undefined') { |
michael@0 | 27 | strings.push('undefined'); |
michael@0 | 28 | } else if (arg === null) { |
michael@0 | 29 | strings.push('null'); |
michael@0 | 30 | } else { |
michael@0 | 31 | try { |
michael@0 | 32 | strings.push(JSON.stringify(arg, null, 2)); |
michael@0 | 33 | } catch(err) { |
michael@0 | 34 | strings.push("<<something>>"); |
michael@0 | 35 | } |
michael@0 | 36 | } |
michael@0 | 37 | }); |
michael@0 | 38 | return strings.join(' '); |
michael@0 | 39 | } |
michael@0 | 40 | |
michael@0 | 41 | if (!gDebug) |
michael@0 | 42 | return; |
michael@0 | 43 | |
michael@0 | 44 | let message = generateLogMessage(pieces); |
michael@0 | 45 | dump(message + "\n"); |
michael@0 | 46 | Services.console.logStringMessage(message); |
michael@0 | 47 | } |
michael@0 | 48 | |
michael@0 | 49 | |
michael@0 | 50 | var observer = { |
michael@0 | 51 | QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, |
michael@0 | 52 | Ci.nsIFormSubmitObserver, |
michael@0 | 53 | Ci.nsISupportsWeakReference]), |
michael@0 | 54 | |
michael@0 | 55 | // nsIFormSubmitObserver |
michael@0 | 56 | notify : function (formElement, aWindow, actionURI) { |
michael@0 | 57 | log("observer notified for form submission."); |
michael@0 | 58 | |
michael@0 | 59 | // We're invoked before the content's |onsubmit| handlers, so we |
michael@0 | 60 | // can grab form data before it might be modified (see bug 257781). |
michael@0 | 61 | |
michael@0 | 62 | try { |
michael@0 | 63 | LoginManagerContent._onFormSubmit(formElement); |
michael@0 | 64 | } catch (e) { |
michael@0 | 65 | log("Caught error in onFormSubmit:", e); |
michael@0 | 66 | } |
michael@0 | 67 | |
michael@0 | 68 | return true; // Always return true, or form submit will be canceled. |
michael@0 | 69 | }, |
michael@0 | 70 | |
michael@0 | 71 | onPrefChange : function() { |
michael@0 | 72 | gDebug = Services.prefs.getBoolPref("signon.debug"); |
michael@0 | 73 | gEnabled = Services.prefs.getBoolPref("signon.rememberSignons"); |
michael@0 | 74 | gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms"); |
michael@0 | 75 | gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff"); |
michael@0 | 76 | }, |
michael@0 | 77 | }; |
michael@0 | 78 | |
michael@0 | 79 | Services.obs.addObserver(observer, "earlyformsubmit", false); |
michael@0 | 80 | var prefBranch = Services.prefs.getBranch("signon."); |
michael@0 | 81 | prefBranch.addObserver("", observer.onPrefChange, false); |
michael@0 | 82 | |
michael@0 | 83 | observer.onPrefChange(); // read initial values |
michael@0 | 84 | |
michael@0 | 85 | |
michael@0 | 86 | var LoginManagerContent = { |
michael@0 | 87 | |
michael@0 | 88 | __formFillService : null, // FormFillController, for username autocompleting |
michael@0 | 89 | get _formFillService() { |
michael@0 | 90 | if (!this.__formFillService) |
michael@0 | 91 | this.__formFillService = |
michael@0 | 92 | Cc["@mozilla.org/satchel/form-fill-controller;1"]. |
michael@0 | 93 | getService(Ci.nsIFormFillController); |
michael@0 | 94 | return this.__formFillService; |
michael@0 | 95 | }, |
michael@0 | 96 | |
michael@0 | 97 | |
michael@0 | 98 | onFormPassword: function (event) { |
michael@0 | 99 | if (!event.isTrusted) |
michael@0 | 100 | return; |
michael@0 | 101 | |
michael@0 | 102 | if (!gEnabled) |
michael@0 | 103 | return; |
michael@0 | 104 | |
michael@0 | 105 | let form = event.target; |
michael@0 | 106 | let doc = form.ownerDocument; |
michael@0 | 107 | |
michael@0 | 108 | log("onFormPassword for", doc.documentURI); |
michael@0 | 109 | |
michael@0 | 110 | // If there are no logins for this site, bail out now. |
michael@0 | 111 | let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI); |
michael@0 | 112 | if (!Services.logins.countLogins(formOrigin, "", null)) |
michael@0 | 113 | return; |
michael@0 | 114 | |
michael@0 | 115 | // If we're currently displaying a master password prompt, defer |
michael@0 | 116 | // processing this form until the user handles the prompt. |
michael@0 | 117 | if (Services.logins.uiBusy) { |
michael@0 | 118 | log("deferring onFormPassword for", doc.documentURI); |
michael@0 | 119 | let self = this; |
michael@0 | 120 | let observer = { |
michael@0 | 121 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), |
michael@0 | 122 | |
michael@0 | 123 | observe: function (subject, topic, data) { |
michael@0 | 124 | log("Got deferred onFormPassword notification:", topic); |
michael@0 | 125 | // Only run observer once. |
michael@0 | 126 | Services.obs.removeObserver(this, "passwordmgr-crypto-login"); |
michael@0 | 127 | Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled"); |
michael@0 | 128 | if (topic == "passwordmgr-crypto-loginCanceled") |
michael@0 | 129 | return; |
michael@0 | 130 | self.onFormPassword(event); |
michael@0 | 131 | }, |
michael@0 | 132 | handleEvent : function (event) { |
michael@0 | 133 | // Not expected to be called |
michael@0 | 134 | } |
michael@0 | 135 | }; |
michael@0 | 136 | // Trickyness follows: We want an observer, but don't want it to |
michael@0 | 137 | // cause leaks. So add the observer with a weak reference, and use |
michael@0 | 138 | // a dummy event listener (a strong reference) to keep it alive |
michael@0 | 139 | // until the form is destroyed. |
michael@0 | 140 | Services.obs.addObserver(observer, "passwordmgr-crypto-login", true); |
michael@0 | 141 | Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true); |
michael@0 | 142 | form.addEventListener("mozCleverClosureHack", observer); |
michael@0 | 143 | return; |
michael@0 | 144 | } |
michael@0 | 145 | |
michael@0 | 146 | let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView); |
michael@0 | 147 | |
michael@0 | 148 | this._fillForm(form, autofillForm, false, false, null); |
michael@0 | 149 | }, |
michael@0 | 150 | |
michael@0 | 151 | |
michael@0 | 152 | /* |
michael@0 | 153 | * onUsernameInput |
michael@0 | 154 | * |
michael@0 | 155 | * Listens for DOMAutoComplete and blur events on an input field. |
michael@0 | 156 | */ |
michael@0 | 157 | onUsernameInput : function(event) { |
michael@0 | 158 | if (!event.isTrusted) |
michael@0 | 159 | return; |
michael@0 | 160 | |
michael@0 | 161 | if (!gEnabled) |
michael@0 | 162 | return; |
michael@0 | 163 | |
michael@0 | 164 | var acInputField = event.target; |
michael@0 | 165 | |
michael@0 | 166 | // This is probably a bit over-conservatative. |
michael@0 | 167 | if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument)) |
michael@0 | 168 | return; |
michael@0 | 169 | |
michael@0 | 170 | if (!this._isUsernameFieldType(acInputField)) |
michael@0 | 171 | return; |
michael@0 | 172 | |
michael@0 | 173 | var acForm = acInputField.form; |
michael@0 | 174 | if (!acForm) |
michael@0 | 175 | return; |
michael@0 | 176 | |
michael@0 | 177 | // If the username is blank, bail out now -- we don't want |
michael@0 | 178 | // fillForm() to try filling in a login without a username |
michael@0 | 179 | // to filter on (bug 471906). |
michael@0 | 180 | if (!acInputField.value) |
michael@0 | 181 | return; |
michael@0 | 182 | |
michael@0 | 183 | log("onUsernameInput from", event.type); |
michael@0 | 184 | |
michael@0 | 185 | // Make sure the username field fillForm will use is the |
michael@0 | 186 | // same field as the autocomplete was activated on. |
michael@0 | 187 | var [usernameField, passwordField, ignored] = |
michael@0 | 188 | this._getFormFields(acForm, false); |
michael@0 | 189 | if (usernameField == acInputField && passwordField) { |
michael@0 | 190 | // If the user has a master password but itsn't logged in, bail |
michael@0 | 191 | // out now to prevent annoying prompts. |
michael@0 | 192 | if (!Services.logins.isLoggedIn) |
michael@0 | 193 | return; |
michael@0 | 194 | |
michael@0 | 195 | this._fillForm(acForm, true, true, true, null); |
michael@0 | 196 | } else { |
michael@0 | 197 | // Ignore the event, it's for some input we don't care about. |
michael@0 | 198 | } |
michael@0 | 199 | }, |
michael@0 | 200 | |
michael@0 | 201 | |
michael@0 | 202 | /* |
michael@0 | 203 | * _getPasswordFields |
michael@0 | 204 | * |
michael@0 | 205 | * Returns an array of password field elements for the specified form. |
michael@0 | 206 | * If no pw fields are found, or if more than 3 are found, then null |
michael@0 | 207 | * is returned. |
michael@0 | 208 | * |
michael@0 | 209 | * skipEmptyFields can be set to ignore password fields with no value. |
michael@0 | 210 | */ |
michael@0 | 211 | _getPasswordFields : function (form, skipEmptyFields) { |
michael@0 | 212 | // Locate the password fields in the form. |
michael@0 | 213 | var pwFields = []; |
michael@0 | 214 | for (var i = 0; i < form.elements.length; i++) { |
michael@0 | 215 | var element = form.elements[i]; |
michael@0 | 216 | if (!(element instanceof Ci.nsIDOMHTMLInputElement) || |
michael@0 | 217 | element.type != "password") |
michael@0 | 218 | continue; |
michael@0 | 219 | |
michael@0 | 220 | if (skipEmptyFields && !element.value) |
michael@0 | 221 | continue; |
michael@0 | 222 | |
michael@0 | 223 | pwFields[pwFields.length] = { |
michael@0 | 224 | index : i, |
michael@0 | 225 | element : element |
michael@0 | 226 | }; |
michael@0 | 227 | } |
michael@0 | 228 | |
michael@0 | 229 | // If too few or too many fields, bail out. |
michael@0 | 230 | if (pwFields.length == 0) { |
michael@0 | 231 | log("(form ignored -- no password fields.)"); |
michael@0 | 232 | return null; |
michael@0 | 233 | } else if (pwFields.length > 3) { |
michael@0 | 234 | log("(form ignored -- too many password fields. [ got ", |
michael@0 | 235 | pwFields.length, "])"); |
michael@0 | 236 | return null; |
michael@0 | 237 | } |
michael@0 | 238 | |
michael@0 | 239 | return pwFields; |
michael@0 | 240 | }, |
michael@0 | 241 | |
michael@0 | 242 | |
michael@0 | 243 | _isUsernameFieldType: function(element) { |
michael@0 | 244 | if (!(element instanceof Ci.nsIDOMHTMLInputElement)) |
michael@0 | 245 | return false; |
michael@0 | 246 | |
michael@0 | 247 | let fieldType = (element.hasAttribute("type") ? |
michael@0 | 248 | element.getAttribute("type").toLowerCase() : |
michael@0 | 249 | element.type); |
michael@0 | 250 | if (fieldType == "text" || |
michael@0 | 251 | fieldType == "email" || |
michael@0 | 252 | fieldType == "url" || |
michael@0 | 253 | fieldType == "tel" || |
michael@0 | 254 | fieldType == "number") { |
michael@0 | 255 | return true; |
michael@0 | 256 | } |
michael@0 | 257 | return false; |
michael@0 | 258 | }, |
michael@0 | 259 | |
michael@0 | 260 | |
michael@0 | 261 | /* |
michael@0 | 262 | * _getFormFields |
michael@0 | 263 | * |
michael@0 | 264 | * Returns the username and password fields found in the form. |
michael@0 | 265 | * Can handle complex forms by trying to figure out what the |
michael@0 | 266 | * relevant fields are. |
michael@0 | 267 | * |
michael@0 | 268 | * Returns: [usernameField, newPasswordField, oldPasswordField] |
michael@0 | 269 | * |
michael@0 | 270 | * usernameField may be null. |
michael@0 | 271 | * newPasswordField will always be non-null. |
michael@0 | 272 | * oldPasswordField may be null. If null, newPasswordField is just |
michael@0 | 273 | * "theLoginField". If not null, the form is apparently a |
michael@0 | 274 | * change-password field, with oldPasswordField containing the password |
michael@0 | 275 | * that is being changed. |
michael@0 | 276 | */ |
michael@0 | 277 | _getFormFields : function (form, isSubmission) { |
michael@0 | 278 | var usernameField = null; |
michael@0 | 279 | |
michael@0 | 280 | // Locate the password field(s) in the form. Up to 3 supported. |
michael@0 | 281 | // If there's no password field, there's nothing for us to do. |
michael@0 | 282 | var pwFields = this._getPasswordFields(form, isSubmission); |
michael@0 | 283 | if (!pwFields) |
michael@0 | 284 | return [null, null, null]; |
michael@0 | 285 | |
michael@0 | 286 | |
michael@0 | 287 | // Locate the username field in the form by searching backwards |
michael@0 | 288 | // from the first passwordfield, assume the first text field is the |
michael@0 | 289 | // username. We might not find a username field if the user is |
michael@0 | 290 | // already logged in to the site. |
michael@0 | 291 | for (var i = pwFields[0].index - 1; i >= 0; i--) { |
michael@0 | 292 | var element = form.elements[i]; |
michael@0 | 293 | if (this._isUsernameFieldType(element)) { |
michael@0 | 294 | usernameField = element; |
michael@0 | 295 | break; |
michael@0 | 296 | } |
michael@0 | 297 | } |
michael@0 | 298 | |
michael@0 | 299 | if (!usernameField) |
michael@0 | 300 | log("(form -- no username field found)"); |
michael@0 | 301 | |
michael@0 | 302 | |
michael@0 | 303 | // If we're not submitting a form (it's a page load), there are no |
michael@0 | 304 | // password field values for us to use for identifying fields. So, |
michael@0 | 305 | // just assume the first password field is the one to be filled in. |
michael@0 | 306 | if (!isSubmission || pwFields.length == 1) |
michael@0 | 307 | return [usernameField, pwFields[0].element, null]; |
michael@0 | 308 | |
michael@0 | 309 | |
michael@0 | 310 | // Try to figure out WTF is in the form based on the password values. |
michael@0 | 311 | var oldPasswordField, newPasswordField; |
michael@0 | 312 | var pw1 = pwFields[0].element.value; |
michael@0 | 313 | var pw2 = pwFields[1].element.value; |
michael@0 | 314 | var pw3 = (pwFields[2] ? pwFields[2].element.value : null); |
michael@0 | 315 | |
michael@0 | 316 | if (pwFields.length == 3) { |
michael@0 | 317 | // Look for two identical passwords, that's the new password |
michael@0 | 318 | |
michael@0 | 319 | if (pw1 == pw2 && pw2 == pw3) { |
michael@0 | 320 | // All 3 passwords the same? Weird! Treat as if 1 pw field. |
michael@0 | 321 | newPasswordField = pwFields[0].element; |
michael@0 | 322 | oldPasswordField = null; |
michael@0 | 323 | } else if (pw1 == pw2) { |
michael@0 | 324 | newPasswordField = pwFields[0].element; |
michael@0 | 325 | oldPasswordField = pwFields[2].element; |
michael@0 | 326 | } else if (pw2 == pw3) { |
michael@0 | 327 | oldPasswordField = pwFields[0].element; |
michael@0 | 328 | newPasswordField = pwFields[2].element; |
michael@0 | 329 | } else if (pw1 == pw3) { |
michael@0 | 330 | // A bit odd, but could make sense with the right page layout. |
michael@0 | 331 | newPasswordField = pwFields[0].element; |
michael@0 | 332 | oldPasswordField = pwFields[1].element; |
michael@0 | 333 | } else { |
michael@0 | 334 | // We can't tell which of the 3 passwords should be saved. |
michael@0 | 335 | log("(form ignored -- all 3 pw fields differ)"); |
michael@0 | 336 | return [null, null, null]; |
michael@0 | 337 | } |
michael@0 | 338 | } else { // pwFields.length == 2 |
michael@0 | 339 | if (pw1 == pw2) { |
michael@0 | 340 | // Treat as if 1 pw field |
michael@0 | 341 | newPasswordField = pwFields[0].element; |
michael@0 | 342 | oldPasswordField = null; |
michael@0 | 343 | } else { |
michael@0 | 344 | // Just assume that the 2nd password is the new password |
michael@0 | 345 | oldPasswordField = pwFields[0].element; |
michael@0 | 346 | newPasswordField = pwFields[1].element; |
michael@0 | 347 | } |
michael@0 | 348 | } |
michael@0 | 349 | |
michael@0 | 350 | return [usernameField, newPasswordField, oldPasswordField]; |
michael@0 | 351 | }, |
michael@0 | 352 | |
michael@0 | 353 | |
michael@0 | 354 | /* |
michael@0 | 355 | * _isAutoCompleteDisabled |
michael@0 | 356 | * |
michael@0 | 357 | * Returns true if the page requests autocomplete be disabled for the |
michael@0 | 358 | * specified form input. |
michael@0 | 359 | */ |
michael@0 | 360 | _isAutocompleteDisabled : function (element) { |
michael@0 | 361 | if (element && element.hasAttribute("autocomplete") && |
michael@0 | 362 | element.getAttribute("autocomplete").toLowerCase() == "off") |
michael@0 | 363 | return true; |
michael@0 | 364 | |
michael@0 | 365 | return false; |
michael@0 | 366 | }, |
michael@0 | 367 | |
michael@0 | 368 | |
michael@0 | 369 | /* |
michael@0 | 370 | * _onFormSubmit |
michael@0 | 371 | * |
michael@0 | 372 | * Called by the our observer when notified of a form submission. |
michael@0 | 373 | * [Note that this happens before any DOM onsubmit handlers are invoked.] |
michael@0 | 374 | * Looks for a password change in the submitted form, so we can update |
michael@0 | 375 | * our stored password. |
michael@0 | 376 | */ |
michael@0 | 377 | _onFormSubmit : function (form) { |
michael@0 | 378 | |
michael@0 | 379 | // For E10S this will need to move. |
michael@0 | 380 | function getPrompter(aWindow) { |
michael@0 | 381 | var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. |
michael@0 | 382 | createInstance(Ci.nsILoginManagerPrompter); |
michael@0 | 383 | prompterSvc.init(aWindow); |
michael@0 | 384 | return prompterSvc; |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | var doc = form.ownerDocument; |
michael@0 | 388 | var win = doc.defaultView; |
michael@0 | 389 | |
michael@0 | 390 | if (PrivateBrowsingUtils.isWindowPrivate(win)) { |
michael@0 | 391 | // We won't do anything in private browsing mode anyway, |
michael@0 | 392 | // so there's no need to perform further checks. |
michael@0 | 393 | log("(form submission ignored in private browsing mode)"); |
michael@0 | 394 | return; |
michael@0 | 395 | } |
michael@0 | 396 | |
michael@0 | 397 | // If password saving is disabled (globally or for host), bail out now. |
michael@0 | 398 | if (!gEnabled) |
michael@0 | 399 | return; |
michael@0 | 400 | |
michael@0 | 401 | var hostname = LoginUtils._getPasswordOrigin(doc.documentURI); |
michael@0 | 402 | if (!hostname) { |
michael@0 | 403 | log("(form submission ignored -- invalid hostname)"); |
michael@0 | 404 | return; |
michael@0 | 405 | } |
michael@0 | 406 | |
michael@0 | 407 | // Somewhat gross hack - we don't want to show the "remember password" |
michael@0 | 408 | // notification on about:accounts for Firefox. |
michael@0 | 409 | let topWin = win.top; |
michael@0 | 410 | if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) { |
michael@0 | 411 | log("(form submission ignored -- about:accounts)"); |
michael@0 | 412 | return; |
michael@0 | 413 | } |
michael@0 | 414 | |
michael@0 | 415 | var formSubmitURL = LoginUtils._getActionOrigin(form) |
michael@0 | 416 | if (!Services.logins.getLoginSavingEnabled(hostname)) { |
michael@0 | 417 | log("(form submission ignored -- saving is disabled for:", hostname, ")"); |
michael@0 | 418 | return; |
michael@0 | 419 | } |
michael@0 | 420 | |
michael@0 | 421 | |
michael@0 | 422 | // Get the appropriate fields from the form. |
michael@0 | 423 | var [usernameField, newPasswordField, oldPasswordField] = |
michael@0 | 424 | this._getFormFields(form, true); |
michael@0 | 425 | |
michael@0 | 426 | // Need at least 1 valid password field to do anything. |
michael@0 | 427 | if (newPasswordField == null) |
michael@0 | 428 | return; |
michael@0 | 429 | |
michael@0 | 430 | // Check for autocomplete=off attribute. We don't use it to prevent |
michael@0 | 431 | // autofilling (for existing logins), but won't save logins when it's |
michael@0 | 432 | // present and the storeWhenAutocompleteOff pref is false. |
michael@0 | 433 | // XXX spin out a bug that we don't update timeLastUsed in this case? |
michael@0 | 434 | if ((this._isAutocompleteDisabled(form) || |
michael@0 | 435 | this._isAutocompleteDisabled(usernameField) || |
michael@0 | 436 | this._isAutocompleteDisabled(newPasswordField) || |
michael@0 | 437 | this._isAutocompleteDisabled(oldPasswordField)) && |
michael@0 | 438 | !gStoreWhenAutocompleteOff) { |
michael@0 | 439 | log("(form submission ignored -- autocomplete=off found)"); |
michael@0 | 440 | return; |
michael@0 | 441 | } |
michael@0 | 442 | |
michael@0 | 443 | |
michael@0 | 444 | var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. |
michael@0 | 445 | createInstance(Ci.nsILoginInfo); |
michael@0 | 446 | formLogin.init(hostname, formSubmitURL, null, |
michael@0 | 447 | (usernameField ? usernameField.value : ""), |
michael@0 | 448 | newPasswordField.value, |
michael@0 | 449 | (usernameField ? usernameField.name : ""), |
michael@0 | 450 | newPasswordField.name); |
michael@0 | 451 | |
michael@0 | 452 | // If we didn't find a username field, but seem to be changing a |
michael@0 | 453 | // password, allow the user to select from a list of applicable |
michael@0 | 454 | // logins to update the password for. |
michael@0 | 455 | if (!usernameField && oldPasswordField) { |
michael@0 | 456 | |
michael@0 | 457 | var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null); |
michael@0 | 458 | |
michael@0 | 459 | if (logins.length == 0) { |
michael@0 | 460 | // Could prompt to save this as a new password-only login. |
michael@0 | 461 | // This seems uncommon, and might be wrong, so ignore. |
michael@0 | 462 | log("(no logins for this host -- pwchange ignored)"); |
michael@0 | 463 | return; |
michael@0 | 464 | } |
michael@0 | 465 | |
michael@0 | 466 | var prompter = getPrompter(win); |
michael@0 | 467 | |
michael@0 | 468 | if (logins.length == 1) { |
michael@0 | 469 | var oldLogin = logins[0]; |
michael@0 | 470 | formLogin.username = oldLogin.username; |
michael@0 | 471 | formLogin.usernameField = oldLogin.usernameField; |
michael@0 | 472 | |
michael@0 | 473 | prompter.promptToChangePassword(oldLogin, formLogin); |
michael@0 | 474 | } else { |
michael@0 | 475 | prompter.promptToChangePasswordWithUsernames( |
michael@0 | 476 | logins, logins.length, formLogin); |
michael@0 | 477 | } |
michael@0 | 478 | |
michael@0 | 479 | return; |
michael@0 | 480 | } |
michael@0 | 481 | |
michael@0 | 482 | |
michael@0 | 483 | // Look for an existing login that matches the form login. |
michael@0 | 484 | var existingLogin = null; |
michael@0 | 485 | var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null); |
michael@0 | 486 | |
michael@0 | 487 | for (var i = 0; i < logins.length; i++) { |
michael@0 | 488 | var same, login = logins[i]; |
michael@0 | 489 | |
michael@0 | 490 | // If one login has a username but the other doesn't, ignore |
michael@0 | 491 | // the username when comparing and only match if they have the |
michael@0 | 492 | // same password. Otherwise, compare the logins and match even |
michael@0 | 493 | // if the passwords differ. |
michael@0 | 494 | if (!login.username && formLogin.username) { |
michael@0 | 495 | var restoreMe = formLogin.username; |
michael@0 | 496 | formLogin.username = ""; |
michael@0 | 497 | same = formLogin.matches(login, false); |
michael@0 | 498 | formLogin.username = restoreMe; |
michael@0 | 499 | } else if (!formLogin.username && login.username) { |
michael@0 | 500 | formLogin.username = login.username; |
michael@0 | 501 | same = formLogin.matches(login, false); |
michael@0 | 502 | formLogin.username = ""; // we know it's always blank. |
michael@0 | 503 | } else { |
michael@0 | 504 | same = formLogin.matches(login, true); |
michael@0 | 505 | } |
michael@0 | 506 | |
michael@0 | 507 | if (same) { |
michael@0 | 508 | existingLogin = login; |
michael@0 | 509 | break; |
michael@0 | 510 | } |
michael@0 | 511 | } |
michael@0 | 512 | |
michael@0 | 513 | if (existingLogin) { |
michael@0 | 514 | log("Found an existing login matching this form submission"); |
michael@0 | 515 | |
michael@0 | 516 | // Change password if needed. |
michael@0 | 517 | if (existingLogin.password != formLogin.password) { |
michael@0 | 518 | log("...passwords differ, prompting to change."); |
michael@0 | 519 | prompter = getPrompter(win); |
michael@0 | 520 | prompter.promptToChangePassword(existingLogin, formLogin); |
michael@0 | 521 | } else { |
michael@0 | 522 | // Update the lastUsed timestamp. |
michael@0 | 523 | var propBag = Cc["@mozilla.org/hash-property-bag;1"]. |
michael@0 | 524 | createInstance(Ci.nsIWritablePropertyBag); |
michael@0 | 525 | propBag.setProperty("timeLastUsed", Date.now()); |
michael@0 | 526 | propBag.setProperty("timesUsedIncrement", 1); |
michael@0 | 527 | Services.logins.modifyLogin(existingLogin, propBag); |
michael@0 | 528 | } |
michael@0 | 529 | |
michael@0 | 530 | return; |
michael@0 | 531 | } |
michael@0 | 532 | |
michael@0 | 533 | |
michael@0 | 534 | // Prompt user to save login (via dialog or notification bar) |
michael@0 | 535 | prompter = getPrompter(win); |
michael@0 | 536 | prompter.promptToSavePassword(formLogin); |
michael@0 | 537 | }, |
michael@0 | 538 | |
michael@0 | 539 | |
michael@0 | 540 | /* |
michael@0 | 541 | * _fillform |
michael@0 | 542 | * |
michael@0 | 543 | * Fill the form with login information if we can find it. This will find |
michael@0 | 544 | * an array of logins if not given any, otherwise it will use the logins |
michael@0 | 545 | * passed in. The logins are returned so they can be reused for |
michael@0 | 546 | * optimization. Success of action is also returned in format |
michael@0 | 547 | * [success, foundLogins]. autofillForm denotes if we should fill the form |
michael@0 | 548 | * in automatically, ignoreAutocomplete denotes if we should ignore |
michael@0 | 549 | * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo |
michael@0 | 550 | * for optimization |
michael@0 | 551 | */ |
michael@0 | 552 | _fillForm : function (form, autofillForm, ignoreAutocomplete, |
michael@0 | 553 | clobberPassword, foundLogins) { |
michael@0 | 554 | // Heuristically determine what the user/pass fields are |
michael@0 | 555 | // We do this before checking to see if logins are stored, |
michael@0 | 556 | // so that the user isn't prompted for a master password |
michael@0 | 557 | // without need. |
michael@0 | 558 | var [usernameField, passwordField, ignored] = |
michael@0 | 559 | this._getFormFields(form, false); |
michael@0 | 560 | |
michael@0 | 561 | // Need a valid password field to do anything. |
michael@0 | 562 | if (passwordField == null) |
michael@0 | 563 | return [false, foundLogins]; |
michael@0 | 564 | |
michael@0 | 565 | // If the password field is disabled or read-only, there's nothing to do. |
michael@0 | 566 | if (passwordField.disabled || passwordField.readOnly) { |
michael@0 | 567 | log("not filling form, password field disabled or read-only"); |
michael@0 | 568 | return [false, foundLogins]; |
michael@0 | 569 | } |
michael@0 | 570 | |
michael@0 | 571 | // Need to get a list of logins if we weren't given them |
michael@0 | 572 | if (foundLogins == null) { |
michael@0 | 573 | var formOrigin = |
michael@0 | 574 | LoginUtils._getPasswordOrigin(form.ownerDocument.documentURI); |
michael@0 | 575 | var actionOrigin = LoginUtils._getActionOrigin(form); |
michael@0 | 576 | foundLogins = Services.logins.findLogins({}, formOrigin, actionOrigin, null); |
michael@0 | 577 | log("found", foundLogins.length, "matching logins."); |
michael@0 | 578 | } else { |
michael@0 | 579 | log("reusing logins from last form."); |
michael@0 | 580 | } |
michael@0 | 581 | |
michael@0 | 582 | // Discard logins which have username/password values that don't |
michael@0 | 583 | // fit into the fields (as specified by the maxlength attribute). |
michael@0 | 584 | // The user couldn't enter these values anyway, and it helps |
michael@0 | 585 | // with sites that have an extra PIN to be entered (bug 391514) |
michael@0 | 586 | var maxUsernameLen = Number.MAX_VALUE; |
michael@0 | 587 | var maxPasswordLen = Number.MAX_VALUE; |
michael@0 | 588 | |
michael@0 | 589 | // If attribute wasn't set, default is -1. |
michael@0 | 590 | if (usernameField && usernameField.maxLength >= 0) |
michael@0 | 591 | maxUsernameLen = usernameField.maxLength; |
michael@0 | 592 | if (passwordField.maxLength >= 0) |
michael@0 | 593 | maxPasswordLen = passwordField.maxLength; |
michael@0 | 594 | |
michael@0 | 595 | var logins = foundLogins.filter(function (l) { |
michael@0 | 596 | var fit = (l.username.length <= maxUsernameLen && |
michael@0 | 597 | l.password.length <= maxPasswordLen); |
michael@0 | 598 | if (!fit) |
michael@0 | 599 | log("Ignored", l.username, "login: won't fit"); |
michael@0 | 600 | |
michael@0 | 601 | return fit; |
michael@0 | 602 | }, this); |
michael@0 | 603 | |
michael@0 | 604 | |
michael@0 | 605 | // Nothing to do if we have no matching logins available. |
michael@0 | 606 | if (logins.length == 0) |
michael@0 | 607 | return [false, foundLogins]; |
michael@0 | 608 | |
michael@0 | 609 | |
michael@0 | 610 | // The reason we didn't end up filling the form, if any. We include |
michael@0 | 611 | // this in the formInfo object we send with the passwordmgr-found-logins |
michael@0 | 612 | // notification. See the _notifyFoundLogins docs for possible values. |
michael@0 | 613 | var didntFillReason = null; |
michael@0 | 614 | |
michael@0 | 615 | // Attach autocomplete stuff to the username field, if we have |
michael@0 | 616 | // one. This is normally used to select from multiple accounts, |
michael@0 | 617 | // but even with one account we should refill if the user edits. |
michael@0 | 618 | if (usernameField) |
michael@0 | 619 | this._formFillService.markAsLoginManagerField(usernameField); |
michael@0 | 620 | |
michael@0 | 621 | // Don't clobber an existing password. |
michael@0 | 622 | if (passwordField.value && !clobberPassword) { |
michael@0 | 623 | didntFillReason = "existingPassword"; |
michael@0 | 624 | this._notifyFoundLogins(didntFillReason, usernameField, |
michael@0 | 625 | passwordField, foundLogins, null); |
michael@0 | 626 | return [false, foundLogins]; |
michael@0 | 627 | } |
michael@0 | 628 | |
michael@0 | 629 | // If the form has an autocomplete=off attribute in play, don't |
michael@0 | 630 | // fill in the login automatically. We check this after attaching |
michael@0 | 631 | // the autocomplete stuff to the username field, so the user can |
michael@0 | 632 | // still manually select a login to be filled in. |
michael@0 | 633 | var isFormDisabled = false; |
michael@0 | 634 | if (!ignoreAutocomplete && |
michael@0 | 635 | (this._isAutocompleteDisabled(form) || |
michael@0 | 636 | this._isAutocompleteDisabled(usernameField) || |
michael@0 | 637 | this._isAutocompleteDisabled(passwordField))) { |
michael@0 | 638 | |
michael@0 | 639 | isFormDisabled = true; |
michael@0 | 640 | log("form not filled, has autocomplete=off"); |
michael@0 | 641 | } |
michael@0 | 642 | |
michael@0 | 643 | // Variable such that we reduce code duplication and can be sure we |
michael@0 | 644 | // should be firing notifications if and only if we can fill the form. |
michael@0 | 645 | var selectedLogin = null; |
michael@0 | 646 | |
michael@0 | 647 | if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) { |
michael@0 | 648 | // If username was specified in the field, it's disabled or it's readOnly, only fill in the |
michael@0 | 649 | // password if we find a matching login. |
michael@0 | 650 | var username = usernameField.value.toLowerCase(); |
michael@0 | 651 | |
michael@0 | 652 | let matchingLogins = logins.filter(function(l) |
michael@0 | 653 | l.username.toLowerCase() == username); |
michael@0 | 654 | if (matchingLogins.length) { |
michael@0 | 655 | selectedLogin = matchingLogins[0]; |
michael@0 | 656 | } else { |
michael@0 | 657 | didntFillReason = "existingUsername"; |
michael@0 | 658 | log("Password not filled. None of the stored logins match the username already present."); |
michael@0 | 659 | } |
michael@0 | 660 | } else if (logins.length == 1) { |
michael@0 | 661 | selectedLogin = logins[0]; |
michael@0 | 662 | } else { |
michael@0 | 663 | // We have multiple logins. Handle a special case here, for sites |
michael@0 | 664 | // which have a normal user+pass login *and* a password-only login |
michael@0 | 665 | // (eg, a PIN). Prefer the login that matches the type of the form |
michael@0 | 666 | // (user+pass or pass-only) when there's exactly one that matches. |
michael@0 | 667 | let matchingLogins; |
michael@0 | 668 | if (usernameField) |
michael@0 | 669 | matchingLogins = logins.filter(function(l) l.username); |
michael@0 | 670 | else |
michael@0 | 671 | matchingLogins = logins.filter(function(l) !l.username); |
michael@0 | 672 | if (matchingLogins.length == 1) { |
michael@0 | 673 | selectedLogin = matchingLogins[0]; |
michael@0 | 674 | } else { |
michael@0 | 675 | didntFillReason = "multipleLogins"; |
michael@0 | 676 | log("Multiple logins for form, so not filling any."); |
michael@0 | 677 | } |
michael@0 | 678 | } |
michael@0 | 679 | |
michael@0 | 680 | var didFillForm = false; |
michael@0 | 681 | if (selectedLogin && autofillForm && !isFormDisabled) { |
michael@0 | 682 | // Fill the form |
michael@0 | 683 | // Don't modify the username field if it's disabled or readOnly so we preserve its case. |
michael@0 | 684 | if (usernameField && !(usernameField.disabled || usernameField.readOnly)) |
michael@0 | 685 | usernameField.value = selectedLogin.username; |
michael@0 | 686 | passwordField.value = selectedLogin.password; |
michael@0 | 687 | didFillForm = true; |
michael@0 | 688 | } else if (selectedLogin && !autofillForm) { |
michael@0 | 689 | // For when autofillForm is false, but we still have the information |
michael@0 | 690 | // to fill a form, we notify observers. |
michael@0 | 691 | didntFillReason = "noAutofillForms"; |
michael@0 | 692 | Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); |
michael@0 | 693 | log("autofillForms=false but form can be filled; notified observers"); |
michael@0 | 694 | } else if (selectedLogin && isFormDisabled) { |
michael@0 | 695 | // For when autocomplete is off, but we still have the information |
michael@0 | 696 | // to fill a form, we notify observers. |
michael@0 | 697 | didntFillReason = "autocompleteOff"; |
michael@0 | 698 | Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); |
michael@0 | 699 | log("autocomplete=off but form can be filled; notified observers"); |
michael@0 | 700 | } |
michael@0 | 701 | |
michael@0 | 702 | this._notifyFoundLogins(didntFillReason, usernameField, passwordField, |
michael@0 | 703 | foundLogins, selectedLogin); |
michael@0 | 704 | |
michael@0 | 705 | return [didFillForm, foundLogins]; |
michael@0 | 706 | }, |
michael@0 | 707 | |
michael@0 | 708 | |
michael@0 | 709 | /** |
michael@0 | 710 | * Notify observers about an attempt to fill a form that resulted in some |
michael@0 | 711 | * saved logins being found for the form. |
michael@0 | 712 | * |
michael@0 | 713 | * This does not get called if the login manager attempts to fill a form |
michael@0 | 714 | * but does not find any saved logins. It does, however, get called when |
michael@0 | 715 | * the login manager does find saved logins whether or not it actually |
michael@0 | 716 | * fills the form with one of them. |
michael@0 | 717 | * |
michael@0 | 718 | * @param didntFillReason {String} |
michael@0 | 719 | * the reason the login manager didn't fill the form, if any; |
michael@0 | 720 | * if the value of this parameter is null, then the form was filled; |
michael@0 | 721 | * otherwise, this parameter will be one of these values: |
michael@0 | 722 | * existingUsername: the username field already contains a username |
michael@0 | 723 | * that doesn't match any stored usernames |
michael@0 | 724 | * existingPassword: the password field already contains a password |
michael@0 | 725 | * autocompleteOff: autocomplete has been disabled for the form |
michael@0 | 726 | * or its username or password fields |
michael@0 | 727 | * multipleLogins: we have multiple logins for the form |
michael@0 | 728 | * noAutofillForms: the autofillForms pref is set to false |
michael@0 | 729 | * |
michael@0 | 730 | * @param usernameField {HTMLInputElement} |
michael@0 | 731 | * the username field detected by the login manager, if any; |
michael@0 | 732 | * otherwise null |
michael@0 | 733 | * |
michael@0 | 734 | * @param passwordField {HTMLInputElement} |
michael@0 | 735 | * the password field detected by the login manager |
michael@0 | 736 | * |
michael@0 | 737 | * @param foundLogins {Array} |
michael@0 | 738 | * an array of nsILoginInfos that can be used to fill the form |
michael@0 | 739 | * |
michael@0 | 740 | * @param selectedLogin {nsILoginInfo} |
michael@0 | 741 | * the nsILoginInfo that was/would be used to fill the form, if any; |
michael@0 | 742 | * otherwise null; whether or not it was actually used depends on |
michael@0 | 743 | * the value of the didntFillReason parameter |
michael@0 | 744 | */ |
michael@0 | 745 | _notifyFoundLogins : function (didntFillReason, usernameField, |
michael@0 | 746 | passwordField, foundLogins, selectedLogin) { |
michael@0 | 747 | // We need .setProperty(), which is a method on the original |
michael@0 | 748 | // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2 |
michael@0 | 749 | // doesn't inherit from that, so the additional QI is needed. |
michael@0 | 750 | let formInfo = Cc["@mozilla.org/hash-property-bag;1"]. |
michael@0 | 751 | createInstance(Ci.nsIWritablePropertyBag2). |
michael@0 | 752 | QueryInterface(Ci.nsIWritablePropertyBag); |
michael@0 | 753 | |
michael@0 | 754 | formInfo.setPropertyAsACString("didntFillReason", didntFillReason); |
michael@0 | 755 | formInfo.setPropertyAsInterface("usernameField", usernameField); |
michael@0 | 756 | formInfo.setPropertyAsInterface("passwordField", passwordField); |
michael@0 | 757 | formInfo.setProperty("foundLogins", foundLogins.concat()); |
michael@0 | 758 | formInfo.setPropertyAsInterface("selectedLogin", selectedLogin); |
michael@0 | 759 | |
michael@0 | 760 | Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null); |
michael@0 | 761 | }, |
michael@0 | 762 | |
michael@0 | 763 | }; |
michael@0 | 764 | |
michael@0 | 765 | |
michael@0 | 766 | |
michael@0 | 767 | |
michael@0 | 768 | LoginUtils = { |
michael@0 | 769 | /* |
michael@0 | 770 | * _getPasswordOrigin |
michael@0 | 771 | * |
michael@0 | 772 | * Get the parts of the URL we want for identification. |
michael@0 | 773 | */ |
michael@0 | 774 | _getPasswordOrigin : function (uriString, allowJS) { |
michael@0 | 775 | var realm = ""; |
michael@0 | 776 | try { |
michael@0 | 777 | var uri = Services.io.newURI(uriString, null, null); |
michael@0 | 778 | |
michael@0 | 779 | if (allowJS && uri.scheme == "javascript") |
michael@0 | 780 | return "javascript:" |
michael@0 | 781 | |
michael@0 | 782 | realm = uri.scheme + "://" + uri.host; |
michael@0 | 783 | |
michael@0 | 784 | // If the URI explicitly specified a port, only include it when |
michael@0 | 785 | // it's not the default. (We never want "http://foo.com:80") |
michael@0 | 786 | var port = uri.port; |
michael@0 | 787 | if (port != -1) { |
michael@0 | 788 | var handler = Services.io.getProtocolHandler(uri.scheme); |
michael@0 | 789 | if (port != handler.defaultPort) |
michael@0 | 790 | realm += ":" + port; |
michael@0 | 791 | } |
michael@0 | 792 | |
michael@0 | 793 | } catch (e) { |
michael@0 | 794 | // bug 159484 - disallow url types that don't support a hostPort. |
michael@0 | 795 | // (although we handle "javascript:..." as a special case above.) |
michael@0 | 796 | log("Couldn't parse origin for", uriString); |
michael@0 | 797 | realm = null; |
michael@0 | 798 | } |
michael@0 | 799 | |
michael@0 | 800 | return realm; |
michael@0 | 801 | }, |
michael@0 | 802 | |
michael@0 | 803 | _getActionOrigin : function (form) { |
michael@0 | 804 | var uriString = form.action; |
michael@0 | 805 | |
michael@0 | 806 | // A blank or missing action submits to where it came from. |
michael@0 | 807 | if (uriString == "") |
michael@0 | 808 | uriString = form.baseURI; // ala bug 297761 |
michael@0 | 809 | |
michael@0 | 810 | return this._getPasswordOrigin(uriString, true); |
michael@0 | 811 | }, |
michael@0 | 812 | |
michael@0 | 813 | }; |