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