Source code

Revision control

Other Tools

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
/**
6
* Module doing most of the content process work for the password manager.
7
*/
8
9
// Disable use-ownerGlobal since LoginForm doesn't have it.
10
/* eslint-disable mozilla/use-ownerGlobal */
11
12
"use strict";
13
14
const EXPORTED_SYMBOLS = ["LoginManagerContent"];
15
16
const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
17
const AUTOCOMPLETE_AFTER_RIGHT_CLICK_THRESHOLD_MS = 400;
18
const AUTOFILL_STATE = "-moz-autofill";
19
20
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
21
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
22
const {PrivateBrowsingUtils} = ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
23
const {PromiseUtils} = ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
24
const {CreditCard} = ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
25
26
ChromeUtils.defineModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
27
ChromeUtils.defineModuleGetter(this, "FormLikeFactory",
29
ChromeUtils.defineModuleGetter(this, "LoginFormFactory",
31
ChromeUtils.defineModuleGetter(this, "LoginRecipesContent",
33
ChromeUtils.defineModuleGetter(this, "LoginHelper",
35
ChromeUtils.defineModuleGetter(this, "InsecurePasswordUtils",
37
ChromeUtils.defineModuleGetter(this, "ContentDOMReference",
39
40
XPCOMUtils.defineLazyServiceGetter(this, "gNetUtil",
41
"@mozilla.org/network/util;1",
42
"nsINetUtil");
43
44
XPCOMUtils.defineLazyGetter(this, "log", () => {
45
let logger = LoginHelper.createLogger("LoginManagerContent");
46
return logger.log.bind(logger);
47
});
48
49
Services.cpmm.addMessageListener("clearRecipeCache", () => {
50
LoginRecipesContent._clearRecipeCache();
51
});
52
53
let gLastRightClickTimeStamp = Number.NEGATIVE_INFINITY;
54
55
const observer = {
56
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver,
57
Ci.nsIWebProgressListener,
58
Ci.nsISupportsWeakReference]),
59
60
61
// nsIWebProgressListener
62
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
63
// Only handle pushState/replaceState here.
64
if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ||
65
!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) {
66
return;
67
}
68
69
log("onLocationChange handled:", aLocation.displaySpec, aWebProgress.DOMWindow.document);
70
71
LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
72
},
73
74
onStateChange(aWebProgress, aRequest, aState, aStatus) {
75
if ((aState & Ci.nsIWebProgressListener.STATE_RESTORING) &&
76
(aState & Ci.nsIWebProgressListener.STATE_STOP)) {
77
// Re-fill a document restored from bfcache since password field values
78
// aren't persisted there.
79
LoginManagerContent._onDocumentRestored(aWebProgress.DOMWindow.document);
80
return;
81
}
82
83
if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
84
return;
85
}
86
87
// We only care about when a page triggered a load, not the user. For example:
88
// clicking refresh/back/forward, typing a URL and hitting enter, and loading a bookmark aren't
89
// likely to be when a user wants to save a login.
90
let channel = aRequest.QueryInterface(Ci.nsIChannel);
91
let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
92
if (triggeringPrincipal.isNullPrincipal ||
93
triggeringPrincipal.equals(Services.scriptSecurityManager.getSystemPrincipal())) {
94
return;
95
}
96
97
// Don't handle history navigation, reload, or pushState not triggered via chrome UI.
98
// e.g. history.go(-1), location.reload(), history.replaceState()
99
if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) {
100
log("onStateChange: loadType isn't LOAD_CMD_NORMAL:", aWebProgress.loadType);
101
return;
102
}
103
104
log("onStateChange handled:", channel);
105
LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
106
},
107
108
// nsIObserver
109
observe(subject, topic, data) {
110
switch (topic) {
111
case "autocomplete-did-enter-text": {
112
let input = subject.QueryInterface(Ci.nsIAutoCompleteInput);
113
let {selectedIndex} = input.popup;
114
if (selectedIndex < 0) {
115
break;
116
}
117
118
let {focusedInput} = LoginManagerContent._formFillService;
119
if (focusedInput.nodePrincipal.isNullPrincipal) {
120
// If we have a null principal then prevent any more password manager code from running and
121
// incorrectly using the document `location`.
122
return;
123
}
124
125
let style = input.controller.getStyleAt(selectedIndex);
126
if (style == "login" || style == "loginWithOrigin") {
127
let details = JSON.parse(input.controller.getCommentAt(selectedIndex));
128
LoginManagerContent.onFieldAutoComplete(focusedInput, details.guid);
129
} else if (style == "generatedPassword") {
130
LoginManagerContent._generatedPasswordFilled(focusedInput);
131
}
132
break;
133
}
134
}
135
},
136
137
// nsIDOMEventListener
138
handleEvent(aEvent) {
139
if (!aEvent.isTrusted) {
140
return;
141
}
142
143
if (!LoginHelper.enabled) {
144
return;
145
}
146
147
switch (aEvent.type) {
148
case "keydown": {
149
if (aEvent.keyCode == aEvent.DOM_VK_TAB ||
150
aEvent.keyCode == aEvent.DOM_VK_RETURN) {
151
LoginManagerContent.onUsernameAutocompleted(aEvent.target);
152
}
153
break;
154
}
155
156
// Only used for username fields.
157
case "focus": {
158
LoginManagerContent._onUsernameFocus(aEvent);
159
break;
160
}
161
162
case "mousedown": {
163
if (aEvent.button == 2) {
164
// Date.now() is used instead of event.timeStamp since
165
// dom.event.highrestimestamp.enabled isn't true on all channels yet.
166
gLastRightClickTimeStamp = Date.now();
167
}
168
169
break;
170
}
171
172
default: {
173
throw new Error("Unexpected event");
174
}
175
}
176
},
177
};
178
179
// Add this observer once for the process.
180
Services.obs.addObserver(observer, "autocomplete-did-enter-text");
181
182
// This object maps to the "child" process (even in the single-process case).
183
this.LoginManagerContent = {
184
__formFillService: null, // FormFillController, for username autocompleting
185
get _formFillService() {
186
if (!this.__formFillService) {
187
this.__formFillService =
188
Cc["@mozilla.org/satchel/form-fill-controller;1"].
189
getService(Ci.nsIFormFillController);
190
}
191
return this.__formFillService;
192
},
193
194
_getRandomId() {
195
return Cc["@mozilla.org/uuid-generator;1"]
196
.getService(Ci.nsIUUIDGenerator).generateUUID().toString();
197
},
198
199
_messages: [
200
"PasswordManager:loginsFound",
201
"PasswordManager:loginsAutoCompleted",
202
],
203
204
/**
205
* WeakMap of the root element of a LoginForm to the DeferredTask to fill its fields.
206
*
207
* This is used to be able to throttle fills for a LoginForm since onDOMInputPasswordAdded gets
208
* dispatched for each password field added to a document but we only want to fill once per
209
* LoginForm when multiple fields are added at once.
210
*
211
* @type {WeakMap}
212
*/
213
_deferredPasswordAddedTasksByRootElement: new WeakMap(),
214
215
/**
216
* WeakMap of a document to the array of callbacks to execute when it becomes visible
217
*
218
* This is used to defer handling DOMFormHasPassword and onDOMInputPasswordAdded events when the
219
* containing document is hidden.
220
* When the document first becomes visible, any queued events will be handled as normal.
221
*
222
* @type {WeakMap}
223
*/
224
_onVisibleTasksByDocument: new WeakMap(),
225
226
// Map from form login requests to information about that request.
227
_requests: new Map(),
228
229
// Number of outstanding requests to each manager.
230
_managers: new Map(),
231
232
// Input element on which enter keydown event was fired.
233
_keyDownEnterForInput: null,
234
235
_takeRequest(msg) {
236
let data = msg.data;
237
let request = this._requests.get(data.requestId);
238
239
this._requests.delete(data.requestId);
240
241
let count = this._managers.get(msg.target);
242
if (--count === 0) {
243
this._managers.delete(msg.target);
244
245
for (let message of this._messages) {
246
msg.target.removeMessageListener(message, this);
247
}
248
} else {
249
this._managers.set(msg.target, count);
250
}
251
252
return request;
253
},
254
255
_sendRequest(messageManager, requestData, name, messageData) {
256
let count;
257
if (!(count = this._managers.get(messageManager))) {
258
this._managers.set(messageManager, 1);
259
260
for (let message of this._messages) {
261
messageManager.addMessageListener(message, this);
262
}
263
} else {
264
this._managers.set(messageManager, ++count);
265
}
266
267
let requestId = this._getRandomId();
268
messageData.requestId = requestId;
269
270
messageManager.sendAsyncMessage(name, messageData);
271
272
let deferred = PromiseUtils.defer();
273
requestData.promise = deferred;
274
this._requests.set(requestId, requestData);
275
return deferred.promise;
276
},
277
278
_onKeyDown(event) {
279
let focusedElement = LoginManagerContent._formFillService.focusedInput;
280
if (event.keyCode != event.DOM_VK_RETURN || focusedElement != event.target) {
281
this._keyDownEnterForInput = null;
282
return;
283
}
284
LoginManagerContent._keyDownEnterForInput = focusedElement;
285
},
286
287
_onPopupClosed(selectedRowStyle, mm) {
288
let focusedElement = LoginManagerContent._formFillService.focusedInput;
289
let eventTarget = LoginManagerContent._keyDownEnterForInput;
290
if (!eventTarget || eventTarget !== focusedElement ||
291
selectedRowStyle != "loginsFooter") {
292
this._keyDownEnterForInput = null;
293
return;
294
}
295
let hostname = eventTarget.ownerDocument.documentURIObject.host;
296
mm.sendAsyncMessage("PasswordManager:OpenPreferences", {
297
hostname,
298
entryPoint: "autocomplete",
299
});
300
},
301
302
receiveMessage(msg, topWindow) {
303
if (msg.name == "PasswordManager:fillForm") {
304
this.fillForm({
305
topDocument: topWindow.document,
306
loginFormOrigin: msg.data.loginFormOrigin,
307
loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
308
recipes: msg.data.recipes,
309
inputElementIdentifier: msg.data.inputElementIdentifier,
310
});
311
return;
312
}
313
314
switch (msg.name) {
315
case "PasswordManager:loginsFound": {
316
let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
317
let request = this._takeRequest(msg);
318
request.promise.resolve({
319
form: request.form,
320
loginsFound,
321
recipes: msg.data.recipes,
322
});
323
break;
324
}
325
326
case "PasswordManager:loginsAutoCompleted": {
327
let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
328
let messageManager = msg.target;
329
let request = this._takeRequest(msg);
330
request.promise.resolve({
331
generatedPassword: msg.data.generatedPassword,
332
logins: loginsFound,
333
messageManager,
334
});
335
break;
336
}
337
338
case "FormAutoComplete:PopupOpened": {
339
let {chromeEventHandler} = msg.target.docShell;
340
chromeEventHandler.addEventListener("keydown", this._onKeyDown,
341
true);
342
break;
343
}
344
345
case "FormAutoComplete:PopupClosed": {
346
this._onPopupClosed(msg.data.selectedRowStyle, msg.target);
347
let {chromeEventHandler} = msg.target.docShell;
348
chromeEventHandler.removeEventListener("keydown", this._onKeyDown,
349
true);
350
break;
351
}
352
}
353
},
354
355
/**
356
* Get relevant logins and recipes from the parent
357
*
358
* @param {HTMLFormElement} form - form to get login data for
359
* @param {Object} options
360
* @param {boolean} options.guid - guid of a login to retrieve
361
* @param {boolean} options.showMasterPassword - whether to show a master password prompt
362
*/
363
_getLoginDataFromParent(form, options) {
364
let doc = form.ownerDocument;
365
let win = doc.defaultView;
366
367
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
368
if (!formOrigin) {
369
return Promise.reject("_getLoginDataFromParent: A form origin is required");
370
}
371
let actionOrigin = LoginHelper.getFormActionOrigin(form);
372
373
let messageManager = win.docShell.messageManager;
374
375
// XXX Weak??
376
let requestData = { form };
377
let messageData = { formOrigin,
378
actionOrigin,
379
options };
380
381
return this._sendRequest(messageManager, requestData,
382
"PasswordManager:findLogins",
383
messageData);
384
},
385
386
_autoCompleteSearchAsync(aSearchString, aPreviousResult,
387
aElement) {
388
let doc = aElement.ownerDocument;
389
let form = LoginFormFactory.createFromField(aElement);
390
let win = doc.defaultView;
391
392
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
393
let actionOrigin = LoginHelper.getFormActionOrigin(form);
394
let autocompleteInfo = aElement.getAutocompleteInfo();
395
396
let messageManager = win.docShell.messageManager;
397
398
let previousResult = aPreviousResult ?
399
{ searchString: aPreviousResult.searchString,
400
logins: LoginHelper.loginsToVanillaObjects(aPreviousResult.logins) } :
401
null;
402
403
let requestData = {};
404
let messageData = {
405
autocompleteInfo,
406
browsingContextId: win.docShell.browsingContext.id,
407
formOrigin,
408
actionOrigin,
409
searchString: aSearchString,
410
previousResult,
411
isSecure: InsecurePasswordUtils.isFormSecure(form),
412
isPasswordField: aElement.type == "password",
413
};
414
415
if (LoginHelper.showAutoCompleteFooter) {
416
messageManager.addMessageListener("FormAutoComplete:PopupOpened", this);
417
messageManager.addMessageListener("FormAutoComplete:PopupClosed", this);
418
}
419
420
return this._sendRequest(messageManager, requestData,
421
"PasswordManager:autoCompleteLogins",
422
messageData);
423
},
424
425
setupEventListeners(global) {
426
global.addEventListener("pageshow", (event) => {
427
this.onPageShow(event);
428
});
429
},
430
431
setupProgressListener(window) {
432
if (!LoginHelper.formlessCaptureEnabled) {
433
return;
434
}
435
436
try {
437
let webProgress = window.docShell.
438
QueryInterface(Ci.nsIInterfaceRequestor).
439
getInterface(Ci.nsIWebProgress);
440
webProgress.addProgressListener(observer,
441
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
442
Ci.nsIWebProgress.NOTIFY_LOCATION);
443
} catch (ex) {
444
// Ignore NS_ERROR_FAILURE if the progress listener was already added
445
}
446
},
447
448
onDOMFormBeforeSubmit(event) {
449
if (!event.isTrusted) {
450
return;
451
}
452
453
// We're invoked before the content's |submit| event handlers, so we
454
// can grab form data before it might be modified (see bug 257781).
455
log("notified before form submission");
456
let formLike = LoginFormFactory.createFromForm(event.target);
457
LoginManagerContent._onFormSubmit(formLike);
458
},
459
460
onDocumentVisibilityChange(event) {
461
if (!event.isTrusted) {
462
return;
463
}
464
let document = event.target;
465
let onVisibleTasks = this._onVisibleTasksByDocument.get(document);
466
if (!onVisibleTasks) {
467
return;
468
}
469
for (let task of onVisibleTasks) {
470
log("onDocumentVisibilityChange, executing queued task");
471
task();
472
}
473
this._onVisibleTasksByDocument.delete(document);
474
},
475
476
_deferHandlingEventUntilDocumentVisible(event, document, fn) {
477
log(`document.visibilityState: ${document.visibilityState}, defer handling ${event.type}`);
478
let onVisibleTasks = this._onVisibleTasksByDocument.get(document);
479
if (!onVisibleTasks) {
480
log(`deferHandling, first queued event, register the visibilitychange handler`);
481
onVisibleTasks = [];
482
this._onVisibleTasksByDocument.set(document, onVisibleTasks);
483
document.addEventListener("visibilitychange", event => {
484
this.onDocumentVisibilityChange(event);
485
}, { once: true });
486
}
487
onVisibleTasks.push(fn);
488
},
489
490
onDOMFormHasPassword(event) {
491
if (!event.isTrusted) {
492
return;
493
}
494
let isMasterPasswordSet = Services.cpmm.sharedData.get("isMasterPasswordSet");
495
let document = event.target.ownerDocument;
496
497
// don't attempt to defer handling when a master password is set
498
// Showing the MP modal as soon as possible minimizes its interference with tab interactions
499
// See bug 1539091 and bug 1538460.
500
log("onDOMFormHasPassword, visibilityState:", document.visibilityState,
501
"isMasterPasswordSet:", isMasterPasswordSet);
502
if (document.visibilityState == "visible" || isMasterPasswordSet) {
503
this._processDOMFormHasPasswordEvent(event);
504
} else {
505
// wait until the document becomes visible before handling this event
506
this._deferHandlingEventUntilDocumentVisible(event, document, () => {
507
this._processDOMFormHasPasswordEvent(event);
508
});
509
}
510
},
511
512
_processDOMFormHasPasswordEvent(event) {
513
let form = event.target;
514
let formLike = LoginFormFactory.createFromForm(form);
515
log("_processDOMFormHasPasswordEvent:", form, formLike);
516
this._fetchLoginsFromParentAndFillForm(formLike);
517
},
518
519
onDOMInputPasswordAdded(event, topWindow) {
520
if (!event.isTrusted) {
521
return;
522
}
523
524
this.setupProgressListener(topWindow);
525
526
let pwField = event.originalTarget;
527
if (pwField.form) {
528
// Fill is handled by onDOMFormHasPassword which is already throttled.
529
return;
530
}
531
532
let document = pwField.ownerDocument;
533
let isMasterPasswordSet = Services.cpmm.sharedData.get("isMasterPasswordSet");
534
log("onDOMInputPasswordAdded, visibilityState:", document.visibilityState,
535
"isMasterPasswordSet:", isMasterPasswordSet);
536
537
// don't attempt to defer handling when a master password is set
538
// Showing the MP modal as soon as possible minimizes its interference with tab interactions
539
// See bug 1539091 and bug 1538460.
540
if (document.visibilityState == "visible" || isMasterPasswordSet) {
541
this._processDOMInputPasswordAddedEvent(event, topWindow);
542
} else {
543
// wait until the document becomes visible before handling this event
544
this._deferHandlingEventUntilDocumentVisible(event, document, () => {
545
this._processDOMInputPasswordAddedEvent(event, topWindow);
546
});
547
}
548
},
549
550
_processDOMInputPasswordAddedEvent(event, topWindow) {
551
let pwField = event.originalTarget;
552
553
let formLike = LoginFormFactory.createFromField(pwField);
554
log(" _processDOMInputPasswordAddedEvent:", pwField, formLike);
555
556
let deferredTask = this._deferredPasswordAddedTasksByRootElement.get(formLike.rootElement);
557
if (!deferredTask) {
558
log("Creating a DeferredTask to call _fetchLoginsFromParentAndFillForm soon");
559
LoginFormFactory.setForRootElement(formLike.rootElement, formLike);
560
561
deferredTask = new DeferredTask(() => {
562
// Get the updated LoginForm instead of the one at the time of creating the DeferredTask via
563
// a closure since it could be stale since LoginForm.elements isn't live.
564
let formLike2 = LoginFormFactory.getForRootElement(formLike.rootElement);
565
log("Running deferred processing of onDOMInputPasswordAdded", formLike2);
566
this._deferredPasswordAddedTasksByRootElement.delete(formLike2.rootElement);
567
this._fetchLoginsFromParentAndFillForm(formLike2);
568
}, PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS, 0);
569
570
this._deferredPasswordAddedTasksByRootElement.set(formLike.rootElement, deferredTask);
571
}
572
573
let window = pwField.ownerGlobal;
574
if (deferredTask.isArmed) {
575
log("DeferredTask is already armed so just updating the LoginForm");
576
// We update the LoginForm so it (most important .elements) is fresh when the task eventually
577
// runs since changes to the elements could affect our field heuristics.
578
LoginFormFactory.setForRootElement(formLike.rootElement, formLike);
579
} else if (window.document.readyState == "complete") {
580
log("Arming the DeferredTask we just created since document.readyState == 'complete'");
581
deferredTask.arm();
582
} else {
583
window.addEventListener("DOMContentLoaded", function() {
584
log("Arming the onDOMInputPasswordAdded DeferredTask due to DOMContentLoaded");
585
deferredTask.arm();
586
}, {once: true});
587
}
588
},
589
590
/**
591
* Fetch logins from the parent for a given form and then attempt to fill it.
592
*
593
* @param {LoginForm} form to fetch the logins for then try autofill.
594
*/
595
_fetchLoginsFromParentAndFillForm(form) {
596
let window = form.ownerDocument.defaultView;
597
this._detectInsecureFormLikes(window.top);
598
599
let messageManager = window.docShell.messageManager;
600
messageManager.sendAsyncMessage("LoginStats:LoginEncountered");
601
602
if (!LoginHelper.enabled) {
603
return;
604
}
605
606
this._getLoginDataFromParent(form, { showMasterPassword: true })
607
.then(this.loginsFound.bind(this))
608
.catch(Cu.reportError);
609
},
610
611
onPageShow(event) {
612
let window = event.target.ownerGlobal;
613
this._detectInsecureFormLikes(window);
614
},
615
616
/**
617
* Maps all DOM content documents in this content process, including those in
618
* frames, to the current state used by the Login Manager.
619
*/
620
loginFormStateByDocument: new WeakMap(),
621
622
/**
623
* Retrieves a reference to the state object associated with the given
624
* document. This is initialized to an object with default values.
625
*/
626
stateForDocument(document) {
627
let loginFormState = this.loginFormStateByDocument.get(document);
628
if (!loginFormState) {
629
loginFormState = {
630
/**
631
* Keeps track of filled fields and values.
632
*/
633
fillsByRootElement: new WeakMap(),
634
/**
635
* Keeps track of logins that were last submitted.
636
*/
637
lastSubmittedValuesByRootElement: new WeakMap(),
638
loginFormRootElements: new WeakSet(),
639
};
640
this.loginFormStateByDocument.set(document, loginFormState);
641
}
642
return loginFormState;
643
},
644
645
/**
646
* Compute whether there is an insecure login form on any frame of the current page, and
647
* notify the parent process. This is used to control whether insecure password UI appears.
648
*/
649
_detectInsecureFormLikes(topWindow) {
650
log("_detectInsecureFormLikes", topWindow.location.href);
651
652
// Returns true if this window or any subframes have insecure login forms.
653
let hasInsecureLoginForms = (thisWindow) => {
654
let doc = thisWindow.document;
655
let rootElsWeakSet = LoginFormFactory.getRootElementsWeakSetForDocument(doc);
656
let hasLoginForm = ChromeUtils.nondeterministicGetWeakSetKeys(rootElsWeakSet)
657
.filter(el => el.isConnected).length > 0;
658
return (hasLoginForm && !thisWindow.isSecureContext) ||
659
Array.prototype.some.call(thisWindow.frames,
660
frame => hasInsecureLoginForms(frame));
661
};
662
663
let messageManager = topWindow.docShell.messageManager;
664
messageManager.sendAsyncMessage("PasswordManager:insecureLoginFormPresent", {
665
hasInsecureLoginForms: hasInsecureLoginForms(topWindow),
666
});
667
},
668
669
/**
670
* Perform a password fill upon user request coming from the parent process.
671
* The fill will be in the form previously identified during page navigation.
672
*
673
* @param An object with the following properties:
674
* {
675
* topDocument:
676
* DOM document currently associated to the the top-level window
677
* for which the fill is requested. This may be different from the
678
* document that originally caused the login UI to be displayed.
679
* loginFormOrigin:
680
* String with the origin for which the login UI was displayed.
681
* This must match the origin of the form used for the fill.
682
* loginsFound:
683
* Array containing the login to fill. While other messages may
684
* have more logins, for this use case this is expected to have
685
* exactly one element. The origin of the login may be different
686
* from the origin of the form used for the fill.
687
* recipes:
688
* Fill recipes transmitted together with the original message.
689
* inputElementIdentifier:
690
* An identifier generated for the input element via ContentDOMReference.
691
* }
692
*/
693
fillForm({ topDocument, loginFormOrigin, loginsFound, recipes, inputElementIdentifier }) {
694
if (!inputElementIdentifier) {
695
log("fillForm: No input element specified");
696
return;
697
}
698
699
let inputElement = ContentDOMReference.resolve(inputElementIdentifier);
700
if (!inputElement) {
701
log("fillForm: Could not resolve inputElementIdentifier to a living element.");
702
return;
703
}
704
705
if (LoginHelper.getLoginOrigin(topDocument.documentURI) != loginFormOrigin) {
706
if (!inputElement ||
707
LoginHelper.getLoginOrigin(inputElement.ownerDocument.documentURI) != loginFormOrigin) {
708
log("fillForm: The requested origin doesn't match the one from the",
709
"document. This may mean we navigated to a document from a different",
710
"site before we had a chance to indicate this change in the user",
711
"interface.");
712
return;
713
}
714
}
715
716
let clobberUsername = true;
717
let form = LoginFormFactory.createFromField(inputElement);
718
if (inputElement.type == "password") {
719
clobberUsername = false;
720
}
721
722
this._fillForm(form, loginsFound, recipes, {
723
inputElement,
724
autofillForm: true,
725
clobberUsername,
726
clobberPassword: true,
727
userTriggered: true,
728
});
729
},
730
731
loginsFound({ form, loginsFound, recipes }) {
732
let doc = form.ownerDocument;
733
let autofillForm = LoginHelper.autofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView);
734
735
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
736
LoginRecipesContent.cacheRecipes(formOrigin, doc.defaultView, recipes);
737
738
this._fillForm(form, loginsFound, recipes, {autofillForm});
739
},
740
741
/**
742
* Focus event handler for username fields to decide whether to show autocomplete.
743
* @param {FocusEvent} event
744
*/
745
_onUsernameFocus(event) {
746
let focusedField = event.target;
747
if (!focusedField.mozIsTextField(true) || focusedField.readOnly) {
748
return;
749
}
750
751
if (this._isLoginAlreadyFilled(focusedField)) {
752
log("_onUsernameFocus: Already filled");
753
return;
754
}
755
756
/*
757
* A `mousedown` event is fired before the `focus` event if the user right clicks into an
758
* unfocused field. In that case we don't want to show both autocomplete and a context menu
759
* overlapping so we check against the timestamp that was set by the `mousedown` event if the
760
* button code indicated a right click.
761
* We use a timestamp instead of a bool to avoid complexity when dealing with multiple input
762
* forms and the fact that a mousedown into an already focused field does not trigger another focus.
763
* Date.now() is used instead of event.timeStamp since dom.event.highrestimestamp.enabled isn't
764
* true on all channels yet.
765
*/
766
let timeDiff = Date.now() - gLastRightClickTimeStamp;
767
if (timeDiff < AUTOCOMPLETE_AFTER_RIGHT_CLICK_THRESHOLD_MS) {
768
log("Not opening autocomplete after focus since a context menu was opened within",
769
timeDiff, "ms");
770
return;
771
}
772
773
log("maybeOpenAutocompleteAfterFocus: Opening the autocomplete popup");
774
this._formFillService.showPopup();
775
},
776
777
/**
778
* A username or password was autocompleted into a field.
779
*/
780
onFieldAutoComplete(acInputField, loginGUID) {
781
if (!LoginHelper.enabled) {
782
return;
783
}
784
785
// This is probably a bit over-conservatative.
786
if (ChromeUtils.getClassName(acInputField.ownerDocument) != "HTMLDocument") {
787
return;
788
}
789
790
if (!LoginFormFactory.createFromField(acInputField)) {
791
return;
792
}
793
794
if (LoginHelper.isUsernameFieldType(acInputField)) {
795
this.onUsernameAutocompleted(acInputField, loginGUID);
796
} else if (acInputField.hasBeenTypePassword) {
797
this._highlightFilledField(acInputField);
798
}
799
},
800
801
/**
802
* A username field was filled or tabbed away from so try fill in the
803
* associated password in the password field.
804
*/
805
onUsernameAutocompleted(acInputField, loginGUID = null) {
806
log("onUsernameAutocompleted:", acInputField);
807
808
let acForm = LoginFormFactory.createFromField(acInputField);
809
let doc = acForm.ownerDocument;
810
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
811
let recipes = LoginRecipesContent.getRecipes(formOrigin, doc.defaultView);
812
813
// Make sure the username field fillForm will use is the
814
// same field as the autocomplete was activated on.
815
let [usernameField, passwordField, ignored] =
816
this._getFormFields(acForm, false, recipes);
817
if (usernameField == acInputField && passwordField) {
818
this._getLoginDataFromParent(acForm, {
819
guid: loginGUID,
820
showMasterPassword: false,
821
})
822
.then(({ form, loginsFound, recipes }) => {
823
this._fillForm(form, loginsFound, recipes, {
824
autofillForm: true,
825
clobberPassword: true,
826
userTriggered: true,
827
});
828
})
829
.catch(Cu.reportError);
830
} else {
831
// Ignore the event, it's for some input we don't care about.
832
}
833
},
834
835
/**
836
* @param {LoginForm} form - the LoginForm to look for password fields in.
837
* @param {Object} options
838
* @param {bool} [options.skipEmptyFields=false] - Whether to ignore password fields with no value.
839
* Used at capture time since saving empty values isn't
840
* useful.
841
* @param {Object} [options.fieldOverrideRecipe=null] - A relevant field override recipe to use.
842
* @return {Array|null} Array of password field elements for the specified form.
843
* If no pw fields are found, or if more than 3 are found, then null
844
* is returned.
845
*/
846
_getPasswordFields(form, {
847
fieldOverrideRecipe = null,
848
minPasswordLength = 0,
849
} = {}) {
850
// Locate the password fields in the form.
851
let pwFields = [];
852
for (let i = 0; i < form.elements.length; i++) {
853
let element = form.elements[i];
854
if (ChromeUtils.getClassName(element) !== "HTMLInputElement" ||
855
element.type != "password" ||
856
!element.isConnected) {
857
continue;
858
}
859
860
// Exclude ones matching a `notPasswordSelector`, if specified.
861
if (fieldOverrideRecipe && fieldOverrideRecipe.notPasswordSelector &&
862
element.matches(fieldOverrideRecipe.notPasswordSelector)) {
863
log("skipping password field (id/name is", element.id, " / ",
864
element.name + ") due to recipe:", fieldOverrideRecipe);
865
continue;
866
}
867
868
// XXX: Bug 780449 tracks our handling of emoji and multi-code-point characters in
869
// password fields. To avoid surprises, we should be consistent with the visual
870
// representation of the masked password
871
if (minPasswordLength && element.value.trim().length < minPasswordLength) {
872
log("skipping password field (id/name is", element.id, " / ",
873
element.name + ") as value is too short:", element.value.trim().length);
874
continue; // Ignore empty or too-short passwords fields
875
}
876
877
pwFields[pwFields.length] = {
878
index: i,
879
element,
880
};
881
}
882
883
// If too few or too many fields, bail out.
884
if (pwFields.length == 0) {
885
log("(form ignored -- no password fields.)");
886
return null;
887
} else if (pwFields.length > 3) {
888
log("(form ignored -- too many password fields. [ got ", pwFields.length, "])");
889
return null;
890
}
891
892
return pwFields;
893
},
894
895
/**
896
* Returns the username and password fields found in the form.
897
* Can handle complex forms by trying to figure out what the
898
* relevant fields are.
899
*
900
* @param {LoginForm} form
901
* @param {bool} isSubmission
902
* @param {Set} recipes
903
* @return {Array} [usernameField, newPasswordField, oldPasswordField]
904
*
905
* usernameField may be null.
906
* newPasswordField will always be non-null.
907
* oldPasswordField may be null. If null, newPasswordField is just
908
* "theLoginField". If not null, the form is apparently a
909
* change-password field, with oldPasswordField containing the password
910
* that is being changed.
911
*
912
* Note that even though we can create a LoginForm from a text field,
913
* this method will only return a non-null usernameField if the
914
* LoginForm has a password field.
915
*/
916
_getFormFields(form, isSubmission, recipes) {
917
let usernameField = null;
918
let pwFields = null;
919
let fieldOverrideRecipe = LoginRecipesContent.getFieldOverrides(recipes, form);
920
if (fieldOverrideRecipe) {
921
let pwOverrideField = LoginRecipesContent.queryLoginField(
922
form,
923
fieldOverrideRecipe.passwordSelector
924
);
925
if (pwOverrideField) {
926
// The field from the password override may be in a different LoginForm.
927
let formLike = LoginFormFactory.createFromField(pwOverrideField);
928
pwFields = [{
929
index: [...formLike.elements].indexOf(pwOverrideField),
930
element: pwOverrideField,
931
}];
932
}
933
934
let usernameOverrideField = LoginRecipesContent.queryLoginField(
935
form,
936
fieldOverrideRecipe.usernameSelector
937
);
938
if (usernameOverrideField) {
939
usernameField = usernameOverrideField;
940
}
941
}
942
943
if (!pwFields) {
944
// Locate the password field(s) in the form. Up to 3 supported.
945
// If there's no password field, there's nothing for us to do.
946
const minSubmitPasswordLength = 2;
947
pwFields = this._getPasswordFields(form, {
948
fieldOverrideRecipe,
949
minPasswordLength: isSubmission ? minSubmitPasswordLength : 0,
950
});
951
}
952
953
if (!pwFields) {
954
return [null, null, null];
955
}
956
957
if (!usernameField) {
958
// Locate the username field in the form by searching backwards
959
// from the first password field, assume the first text field is the
960
// username. We might not find a username field if the user is
961
// already logged in to the site.
962
963
for (let i = pwFields[0].index - 1; i >= 0; i--) {
964
let element = form.elements[i];
965
if (!LoginHelper.isUsernameFieldType(element)) {
966
continue;
967
}
968
969
if (fieldOverrideRecipe && fieldOverrideRecipe.notUsernameSelector &&
970
element.matches(fieldOverrideRecipe.notUsernameSelector)) {
971
continue;
972
}
973
974
usernameField = element;
975
break;
976
}
977
}
978
979
if (!usernameField) {
980
log("(form -- no username field found)");
981
} else {
982
let acFieldName = usernameField.getAutocompleteInfo().fieldName;
983
log("Username field ", usernameField, "has name/value/autocomplete:",
984
usernameField.name, "/", usernameField.value, "/", acFieldName);
985
}
986
// If we're not submitting a form (it's a page load), there are no
987
// password field values for us to use for identifying fields. So,
988
// just assume the first password field is the one to be filled in.
989
if (!isSubmission || pwFields.length == 1) {
990
let passwordField = pwFields[0].element;
991
log("Password field", passwordField, "has name: ", passwordField.name);
992
return [usernameField, passwordField, null];
993
}
994
995
996
// Try to figure out WTF is in the form based on the password values.
997
let oldPasswordField, newPasswordField;
998
let pw1 = pwFields[0].element.value;
999
let pw2 = pwFields[1].element.value;
1000
let pw3 = (pwFields[2] ? pwFields[2].element.value : null);
1001
1002
if (pwFields.length == 3) {
1003
// Look for two identical passwords, that's the new password
1004
1005
if (pw1 == pw2 && pw2 == pw3) {
1006
// All 3 passwords the same? Weird! Treat as if 1 pw field.
1007
newPasswordField = pwFields[0].element;
1008
oldPasswordField = null;
1009
} else if (pw1 == pw2) {
1010
newPasswordField = pwFields[0].element;
1011
oldPasswordField = pwFields[2].element;
1012
} else if (pw2 == pw3) {
1013
oldPasswordField = pwFields[0].element;
1014
newPasswordField = pwFields[2].element;
1015
} else if (pw1 == pw3) {
1016
// A bit odd, but could make sense with the right page layout.
1017
newPasswordField = pwFields[0].element;
1018
oldPasswordField = pwFields[1].element;
1019
} else {
1020
// We can't tell which of the 3 passwords should be saved.
1021
log("(form ignored -- all 3 pw fields differ)");
1022
return [null, null, null];
1023
}
1024
} else if (pw1 == pw2) {
1025
// pwFields.length == 2
1026
// Treat as if 1 pw field
1027
newPasswordField = pwFields[0].element;
1028
oldPasswordField = null;
1029
} else {
1030
// Just assume that the 2nd password is the new password
1031
oldPasswordField = pwFields[0].element;
1032
newPasswordField = pwFields[1].element;
1033
}
1034
1035
log("Password field (new) id/name is: ", newPasswordField.id, " / ", newPasswordField.name);
1036
if (oldPasswordField) {
1037
log("Password field (old) id/name is: ", oldPasswordField.id, " / ", oldPasswordField.name);
1038
} else {
1039
log("Password field (old):", oldPasswordField);
1040
}
1041
return [usernameField, newPasswordField, oldPasswordField];
1042
},
1043
1044
1045
/**
1046
* @return true if the page requests autocomplete be disabled for the
1047
* specified element.
1048
*/
1049
_isAutocompleteDisabled(element) {
1050
return element && element.autocomplete == "off";
1051
},
1052
1053
/**
1054
* Fill a page that was restored from bfcache since we wouldn't receive
1055
* DOMInputPasswordAdded or DOMFormHasPassword events for it.
1056
* @param {Document} aDocument that was restored from bfcache.
1057
*/
1058
_onDocumentRestored(aDocument) {
1059
let rootElsWeakSet = LoginFormFactory.getRootElementsWeakSetForDocument(aDocument);
1060
let weakLoginFormRootElements = ChromeUtils.nondeterministicGetWeakSetKeys(rootElsWeakSet);
1061
1062
log("_onDocumentRestored: loginFormRootElements approx size:", weakLoginFormRootElements.length,
1063
"document:", aDocument);
1064
1065
for (let formRoot of weakLoginFormRootElements) {
1066
if (!formRoot.isConnected) {
1067
continue;
1068
}
1069
1070
let formLike = LoginFormFactory.getForRootElement(formRoot);
1071
this._fetchLoginsFromParentAndFillForm(formLike);
1072
}
1073
},
1074
1075
/**
1076
* Trigger capture on any relevant FormLikes due to a navigation alone (not
1077
* necessarily due to an actual form submission). This method is used to
1078
* capture logins for cases where form submit events are not used.
1079
*
1080
* To avoid multiple notifications for the same LoginForm, this currently
1081
* avoids capturing when dealing with a real <form> which are ideally already
1082
* using a submit event.
1083
*
1084
* @param {Document} document being navigated
1085
*/
1086
_onNavigation(aDocument) {
1087
let rootElsWeakSet = LoginFormFactory.getRootElementsWeakSetForDocument(aDocument);
1088
let weakLoginFormRootElements = ChromeUtils.nondeterministicGetWeakSetKeys(rootElsWeakSet);
1089
1090
log("_onNavigation: root elements approx size:", weakLoginFormRootElements.length,
1091
"document:", aDocument);
1092
1093
for (let formRoot of weakLoginFormRootElements) {
1094
if (!formRoot.isConnected) {
1095
continue;
1096
}
1097
1098
let formLike = LoginFormFactory.getForRootElement(formRoot);
1099
this._onFormSubmit(formLike);
1100
}
1101
},
1102
1103
/**
1104
* Called by our observer when notified of a form submission.
1105
* [Note that this happens before any DOM onsubmit handlers are invoked.]
1106
* Looks for a password change in the submitted form, so we can update
1107
* our stored password.
1108
*
1109
* @param {LoginForm} form
1110
*/
1111
_onFormSubmit(form) {
1112
log("_onFormSubmit", form);
1113
let doc = form.ownerDocument;
1114
let win = doc.defaultView;
1115
1116
if (PrivateBrowsingUtils.isContentWindowPrivate(win) &&
1117
!LoginHelper.privateBrowsingCaptureEnabled) {
1118
// We won't do anything in private browsing mode anyway,
1119
// so there's no need to perform further checks.
1120
log("(form submission ignored in private browsing mode)");
1121
return;
1122
}
1123
1124
// If password saving is disabled globally, bail out now.
1125
if (!LoginHelper.enabled) {
1126
return;
1127
}
1128
1129
let origin = LoginHelper.getLoginOrigin(doc.documentURI);
1130
if (!origin) {
1131
log("(form submission ignored -- invalid origin)");
1132
return;
1133
}
1134
1135
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
1136
let messageManager = win.docShell.messageManager;
1137
1138
let recipes = LoginRecipesContent.getRecipes(origin, win);
1139
1140
// Get the appropriate fields from the form.
1141
let [usernameField, newPasswordField, oldPasswordField] =
1142
this._getFormFields(form, true, recipes);
1143
1144
// Need at least 1 valid password field to do anything.
1145
if (newPasswordField == null) {
1146
return;
1147
}
1148
1149
if (usernameField && usernameField.value.match(/[•\*]{3,}/)) {
1150
log(`usernameField.value "${usernameField.value}" looks munged, setting to null`);
1151
usernameField = null;
1152
}
1153
1154
// Check for autocomplete=off attribute. We don't use it to prevent
1155
// autofilling (for existing logins), but won't save logins when it's
1156
// present and the storeWhenAutocompleteOff pref is false.
1157
// XXX spin out a bug that we don't update timeLastUsed in this case?
1158
if ((this._isAutocompleteDisabled(form) ||
1159
this._isAutocompleteDisabled(usernameField) ||
1160
this._isAutocompleteDisabled(newPasswordField) ||
1161
this._isAutocompleteDisabled(oldPasswordField)) &&
1162
!LoginHelper.storeWhenAutocompleteOff) {
1163
log("(form submission ignored -- autocomplete=off found)");
1164
return;
1165
}
1166
1167
// Don't try to send DOM nodes over IPC.
1168
let mockUsername = usernameField ?
1169
{ name: usernameField.name,
1170
value: usernameField.value } :
1171
null;
1172
let mockPassword = { name: newPasswordField.name,
1173
value: newPasswordField.value };
1174
let mockOldPassword = oldPasswordField ?
1175
{ name: oldPasswordField.name,
1176
value: oldPasswordField.value } :
1177
null;
1178
1179
let usernameValue = usernameField ? usernameField.value : null;
1180
let formLikeRoot = FormLikeFactory.findRootForField(newPasswordField);
1181
let state = this.stateForDocument(doc);
1182
let lastSubmittedValues = state.lastSubmittedValuesByRootElement.get(formLikeRoot);
1183
if (lastSubmittedValues) {
1184
if (lastSubmittedValues.username == usernameValue &&
1185
lastSubmittedValues.password == newPasswordField.value) {
1186
log("(form submission ignored -- already submitted with the same username and password)");
1187
return;
1188
}
1189
}
1190
1191
// Save the last submitted values so we don't prompt twice for the same values using
1192
// different capture methods e.g. a form submit event and upon navigation.
1193
state.lastSubmittedValuesByRootElement.set(formLikeRoot, {
1194
username: usernameValue,
1195
password: newPasswordField.value,
1196
});
1197
1198
// Make sure to pass the opener's top ID in case it was in a frame.
1199
let openerTopWindowID = null;
1200
if (win.opener) {
1201
openerTopWindowID = win.opener.top.windowUtils.outerWindowID;
1202
}
1203
1204
// Dismiss prompt if the username field is a credit card number AND
1205
// if the password field is a three digit number. Also dismiss prompt if
1206
// the password is a credit card number and the password field has attribute
1207
// autocomplete="cc-number".
1208
let dismissedPrompt = false;
1209
let newPasswordFieldValue = newPasswordField.value;
1210
if ((CreditCard.isValidNumber(usernameValue) && newPasswordFieldValue.trim().match(/^[0-9]{3}$/)) ||
1211
(CreditCard.isValidNumber(newPasswordFieldValue) && newPasswordField.getAutocompleteInfo().fieldName == "cc-number")) {
1212
dismissedPrompt = true;
1213
}
1214
1215
let autoFilledLogin = this.stateForDocument(doc).fillsByRootElement.get(form.rootElement);
1216
messageManager.sendAsyncMessage("PasswordManager:onFormSubmit",
1217
{ origin,
1218
formActionOrigin,
1219
autoFilledLoginGuid: autoFilledLogin && autoFilledLogin.guid,
1220
usernameField: mockUsername,
1221
newPasswordField: mockPassword,
1222
oldPasswordField: mockOldPassword,
1223
openerTopWindowID,
1224
dismissedPrompt,
1225
});
1226
},
1227
1228
/**
1229
* Notify the parent that a generated password was filled into a field so that it can potentially
1230
* be saved.
1231
* @param {HTMLInputElement} input
1232
*/
1233
_generatedPasswordFilled(input) {
1234
log("_generatedPasswordFilled", input);
1235
let loginForm = LoginFormFactory.createFromField(input);
1236
let win = input.ownerGlobal;
1237
1238
if (PrivateBrowsingUtils.isContentWindowPrivate(win)) {
1239
log("_generatedPasswordFilled: not automatically saving the password in private browsing mode");
1240
return;
1241
}
1242
1243
if (!LoginHelper.enabled) {
1244
throw new Error("A generated password was filled while the password manager was disabled.");
1245
}
1246
1247
let formActionOrigin = LoginHelper.getFormActionOrigin(loginForm);
1248
let messageManager = win.docShell.messageManager;
1249
messageManager.sendAsyncMessage("PasswordManager:onGeneratedPasswordFilled", {
1250
browsingContextId: win.docShell.browsingContext.id,
1251
formActionOrigin,
1252
});
1253
},
1254
1255
/** Remove login field highlight when its value is cleared or overwritten.
1256
*/
1257
_removeFillFieldHighlight(event) {
1258
let winUtils = event.target.ownerGlobal.windowUtils;
1259
winUtils.removeManuallyManagedState(event.target, AUTOFILL_STATE);
1260
},
1261
1262
/**
1263
* Highlight login fields on autocomplete or autofill on page load.
1264
* @param {Node} element that needs highlighting.
1265
*/
1266
_highlightFilledField(element) {
1267
let winUtils = element.ownerGlobal.windowUtils;
1268
1269
winUtils.addManuallyManagedState(element, AUTOFILL_STATE);
1270
// Remove highlighting when the field is changed.
1271
element.addEventListener("input", this._removeFillFieldHighlight, {
1272
mozSystemGroup: true,
1273
once: true,
1274
});
1275
},
1276
1277
/**
1278
* Attempt to find the username and password fields in a form, and fill them
1279
* in using the provided logins and recipes.
1280
*
1281
* @param {LoginForm} form
1282
* @param {nsILoginInfo[]} foundLogins an array of nsILoginInfo that could be
1283
* used for the form, including ones with a different form action origin
1284
* which are only used when the fill is userTriggered
1285
* @param {Set} recipes a set of recipes that could be used to affect how the
1286
* form is filled
1287
* @param {Object} [options = {}] a list of options for this method
1288
* @param {HTMLInputElement} [options.inputElement = null] an optional target
1289
* input element we want to fill
1290
* @param {bool} [options.autofillForm = false] denotes if we should fill the
1291
* form in automatically
1292
* @param {bool} [options.clobberUsername = false] controls if an existing
1293
* username can be overwritten. If this is false and an inputElement
1294
* of type password is also passed, the username field will be ignored.
1295
* If this is false and no inputElement is passed, if the username
1296
* field value is not found in foundLogins, it will not fill the
1297
* password.
1298
* @param {bool} [options.clobberPassword = false] controls if an existing
1299
* password value can be overwritten
1300
* @param {bool} [options.userTriggered = false] an indication of whether
1301
* this filling was triggered by the user
1302
*/
1303
// eslint-disable-next-line complexity
1304
_fillForm(form, foundLogins, recipes, {
1305
inputElement = null,
1306
autofillForm = false,
1307
clobberUsername = false,
1308
clobberPassword = false,
1309
userTriggered = false,
1310
} = {}) {
1311
if (ChromeUtils.getClassName(form) === "HTMLFormElement") {
1312
throw new Error("_fillForm should only be called with LoginForm objects");
1313
}
1314
1315
log("_fillForm", form.elements);
1316
let usernameField;
1317
// Will be set to one of AUTOFILL_RESULT in the `try` block.
1318
let autofillResult = -1;
1319
const AUTOFILL_RESULT = {
1320
FILLED: 0,
1321
NO_PASSWORD_FIELD: 1,
1322
PASSWORD_DISABLED_READONLY: 2,
1323
NO_LOGINS_FIT: 3,
1324
NO_SAVED_LOGINS: 4,
1325
EXISTING_PASSWORD: 5,
1326
EXISTING_USERNAME: 6,
1327
MULTIPLE_LOGINS: 7,
1328
NO_AUTOFILL_FORMS: 8,
1329
AUTOCOMPLETE_OFF: 9,
1330
INSECURE: 10,
1331
PASSWORD_AUTOCOMPLETE_NEW_PASSWORD: 11,
1332
};
1333
1334
try {
1335
// Nothing to do if we have no matching (excluding form action
1336
// checks) logins available, and there isn't a need to show
1337
// the insecure form warning.
1338
if (foundLogins.length == 0 &&
1339
(InsecurePasswordUtils.isFormSecure(form) ||
1340
!LoginHelper.showInsecureFieldWarning)) {
1341
// We don't log() here since this is a very common case.
1342
autofillResult = AUTOFILL_RESULT.NO_SAVED_LOGINS;
1343
return;
1344
}
1345
1346
// Heuristically determine what the user/pass fields are
1347
// We do this before checking to see if logins are stored,
1348
// so that the user isn't prompted for a master password
1349
// without need.
1350
let passwordField;
1351
[usernameField, passwordField] = this._getFormFields(form, false, recipes);
1352
1353
// If we have a password inputElement parameter and it's not
1354
// the same as the one heuristically found, use the parameter
1355
// one instead.
1356
if (inputElement) {
1357
if (inputElement.type == "password") {
1358
passwordField = inputElement;
1359
if (!clobberUsername) {
1360
usernameField = null;
1361
}
1362
} else if (LoginHelper.isUsernameFieldType(inputElement)) {
1363
usernameField = inputElement;
1364
} else {
1365
throw new Error("Unexpected input element type.");
1366
}
1367
}
1368
1369
// Need a valid password field to do anything.
1370
if (passwordField == null) {
1371
log("not filling form, no password field found");
1372
autofillResult = AUTOFILL_RESULT.NO_PASSWORD_FIELD;
1373
return;
1374
}
1375
1376
// If the password field is disabled or read-only, there's nothing to do.
1377
if (passwordField.disabled || passwordField.readOnly) {
1378
log("not filling form, password field disabled or read-only");
1379
autofillResult = AUTOFILL_RESULT.PASSWORD_DISABLED_READONLY;
1380
return;
1381
}
1382
1383
// Attach autocomplete stuff to the username field, if we have
1384
// one. This is normally used to select from multiple accounts,
1385
// but even with one account we should refill if the user edits.
1386
// We would also need this attached to show the insecure login
1387
// warning, regardless of saved login.
1388
if (usernameField) {
1389
this._formFillService.markAsLoginManagerField(usernameField);
1390
usernameField.addEventListener("keydown", observer);
1391
}
1392
1393
if (!userTriggered) {
1394
// Only autofill logins that match the form's action and origin. In the above code
1395
// we have attached autocomplete for logins that don't match the form action.
1396
let loginOrigin = LoginHelper.getLoginOrigin(form.ownerDocument.documentURI);
1397
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
1398
foundLogins = foundLogins.filter(l => {
1399
let formActionMatches = LoginHelper.isOriginMatching(l.formActionOrigin,
1400
formActionOrigin,
1401
{
1402
schemeUpgrades: LoginHelper.schemeUpgrades,
1403
acceptWildcardMatch: true,
1404
acceptDifferentSubdomains: false,
1405
});
1406
let formOriginMatches = LoginHelper.isOriginMatching(l.origin,
1407
loginOrigin,
1408
{
1409
schemeUpgrades: LoginHelper.schemeUpgrades,
1410
acceptWildcardMatch: true,
1411
acceptDifferentSubdomains: false,
1412
});
1413
return formActionMatches && formOriginMatches;
1414
});
1415
1416
// Since the logins are already filtered now to only match the origin and formAction,
1417
// dedupe to just the username since remaining logins may have different schemes.
1418
foundLogins = LoginHelper.dedupeLogins(foundLogins,
1419
["username"],
1420
[
1421
"scheme",
1422
"timePasswordChanged",
1423
],
1424
loginOrigin,
1425
formActionOrigin);
1426
}
1427
1428
// Nothing to do if we have no matching logins available.
1429
// Only insecure pages reach this block and logs the same
1430
// telemetry flag.
1431
if (foundLogins.length == 0) {
1432
// We don't log() here since this is a very common case.
1433
autofillResult = AUTOFILL_RESULT.NO_SAVED_LOGINS;
1434
return;
1435
}
1436
1437
// Prevent autofilling insecure forms.
1438
if (!userTriggered && !LoginHelper.insecureAutofill &&
1439
!InsecurePasswordUtils.isFormSecure(form)) {
1440
log("not filling form since it's insecure");
1441
autofillResult = AUTOFILL_RESULT.INSECURE;
1442
return;
1443
}
1444
1445
// Discard logins which have username/password values that don't
1446
// fit into the fields (as specified by the maxlength attribute).
1447
// The user couldn't enter these values anyway, and it helps
1448
// with sites that have an extra PIN to be entered (bug 391514)
1449
let maxUsernameLen = Number.MAX_VALUE;
1450
let maxPasswordLen = Number.MAX_VALUE;
1451
1452
// If attribute wasn't set, default is -1.
1453
if (usernameField && usernameField.maxLength >= 0) {
1454
maxUsernameLen = usernameField.maxLength;
1455
}
1456
if (passwordField.maxLength >= 0) {
1457
maxPasswordLen = passwordField.maxLength;
1458
}
1459
1460
let logins = foundLogins.filter(function(l) {
1461
let fit = (l.username.length <= maxUsernameLen &&
1462
l.password.length <= maxPasswordLen);
1463
if (!fit) {
1464
log("Ignored", l.username, "login: won't fit");
1465
}
1466
1467
return fit;
1468
}, this);
1469
1470
if (logins.length == 0) {
1471
log("form not filled, none of the logins fit in the field");
1472
autofillResult = AUTOFILL_RESULT.NO_LOGINS_FIT;
1473
return;
1474
}
1475
1476
const passwordACFieldName = passwordField.getAutocompleteInfo().fieldName;
1477
1478
// If the password field has the autocomplete value of "new-password"
1479
// and we're autofilling without user interaction, there's nothing to do.
1480
if (!userTriggered && passwordACFieldName == "new-password") {
1481
log("not filling form, password field has the autocomplete new-password value");
1482
autofillResult = AUTOFILL_RESULT.PASSWORD_AUTOCOMPLETE_NEW_PASSWORD;
1483
return;
1484
}
1485
1486
// Don't clobber an existing password.
1487
if (passwordField.value && !clobberPassword) {
1488
log("form not filled, the password field was already filled");
1489
autofillResult = AUTOFILL_RESULT.EXISTING_PASSWORD;
1490
return;
1491
}
1492
1493
// Select a login to use for filling in the form.
1494
let selectedLogin;
1495
if (!clobberUsername && usernameField && (usernameField.value ||
1496
usernameField.disabled ||
1497
usernameField.readOnly)) {
1498
// If username was specified in the field, it's disabled or it's readOnly, only fill in the
1499
// password if we find a matching login.
1500
let username = usernameField.value.toLowerCase();
1501
1502
let matchingLogins = logins.filter(l =>
1503
l.username.toLowerCase() == username);
1504
if (matchingLogins.length == 0) {
1505
log("Password not filled. None of the stored logins match the username already present.");
1506
autofillResult = AUTOFILL_RESULT.EXISTING_USERNAME;
1507
return;
1508
}
1509
1510
// If there are multiple, and one matches case, use it
1511
for (let l of matchingLogins) {
1512
if (l.username == usernameField.value) {
1513
selectedLogin = l;
1514
}
1515
}
1516
// Otherwise just use the first
1517
if (!selectedLogin) {
1518
selectedLogin = matchingLogins[0];
1519
}
1520
} else if (logins.length == 1) {
1521
selectedLogin = logins[0];
1522
} else {
1523
// We have multiple logins. Handle a special case here, for sites
1524
// which have a normal user+pass login *and* a password-only login
1525
// (eg, a PIN). Prefer the login that matches the type of the form
1526
// (user+pass or pass-only) when there's exactly one that matches.
1527
let matchingLogins;
1528
if (usernameField) {
1529
matchingLogins = logins.filter(l => l.username);
1530
} else {
1531
matchingLogins = logins.filter(l => !l.username);
1532
}
1533
1534
if (matchingLogins.length != 1) {
1535
log("Multiple logins for form, so not filling any.");
1536
autofillResult = AUTOFILL_RESULT.MULTIPLE_LOGINS;
1537
return;
1538
}
1539
1540
selectedLogin = matchingLogins[0];
1541
}
1542
1543
// We will always have a selectedLogin at this point.
1544
1545
if (!autofillForm) {
1546
log("autofillForms=false but form can be filled");
1547
autofillResult = AUTOFILL_RESULT.NO_AUTOFILL_FORMS;
1548
return;
1549
}
1550
1551
if (!userTriggered && passwordACFieldName == "off" && !LoginHelper.autofillAutocompleteOff) {
1552
log("Not autofilling the login because we're respecting autocomplete=off");
1553
autofillResult = AUTOFILL_RESULT.AUTOCOMPLETE_OFF;
1554
return;
1555
}
1556
1557
// Fill the form
1558
1559
if (usernameField) {
1560
// Don't modify the username field if it's disabled or readOnly so we preserve its case.
1561
let disabledOrReadOnly = usernameField.disabled || usernameField.readOnly;
1562
1563
let userNameDiffers = selectedLogin.username != usernameField.value;
1564
// Don't replace the username if it differs only in case, and the user triggered
1565
// this autocomplete. We assume that if it was user-triggered the entered text
1566
// is desired.
1567
let userEnteredDifferentCase = userTriggered && userNameDiffers &&
1568
usernameField.value.toLowerCase() == selectedLogin.username.toLowerCase();
1569
1570
if (!disabledOrReadOnly) {
1571
if (!userEnteredDifferentCase && userNameDiffers) {
1572
usernameField.setUserInput(selectedLogin.username);
1573
}
1574
1575
this._highlightFilledField(usernameField);
1576
}
1577
}
1578
1579
let doc = form.ownerDocument;
1580
if (passwordField.value != selectedLogin.password) {
1581
passwordField.setUserInput(selectedLogin.password);
1582
let autoFilledLogin = {
1583
guid: selectedLogin.QueryInterface(Ci.nsILoginMetaInfo).guid,
1584
username: selectedLogin.username,
1585
usernameField: usernameField ? Cu.getWeakReference(usernameField) : null,
1586
password: selectedLogin.password,
1587
passwordField: Cu.getWeakReference(passwordField),
1588
};
1589
log("Saving autoFilledLogin", autoFilledLogin.guid, "for", form.rootElement);
1590
this.stateForDocument(doc).fillsByRootElement.set(form.rootElement, autoFilledLogin);
1591
}
1592
1593
this._highlightFilledField(passwordField);
1594
1595
log("_fillForm succeeded");
1596
autofillResult = AUTOFILL_RESULT.FILLED;
1597
1598
let win = doc.defaultView;
1599
let messageManager = win.docShell.messageManager;
1600
messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
1601
} finally {
1602
if (autofillResult == -1) {
1603
// eslint-disable-next-line no-unsafe-finally
1604
throw new Error("_fillForm: autofillResult must be specified");
1605
}
1606
1607
if (!userTriggered) {
1608
// Ignore fills as a result of user action for this probe.
1609
Services.telemetry.getHistogramById("PWMGR_FORM_AUTOFILL_RESULT").add(autofillResult);
1610
1611
if (usernameField) {
1612
let focusedElement = this._formFillService.focusedInput;
1613
if (usernameField == focusedElement &&
1614
autofillResult !== AUTOFILL_RESULT.FILLED) {
1615
log("_fillForm: Opening username autocomplete popup since the form wasn't autofilled");
1616
this._formFillService.showPopup();
1617
}
1618
}
1619
}
1620
1621
if (usernameField) {
1622
log("_fillForm: Attaching event listeners to usernameField");
1623
usernameField.addEventListener("focus", observer);
1624
usernameField.addEventListener("mousedown", observer);
1625
}
1626
1627
Services.obs.notifyObservers(form.rootElement, "passwordmgr-processed-form");
1628
}
1629
},
1630
1631
/**
1632
* Given a field, determine whether that field was last filled as a username
1633
* field AND whether the username is still filled in with the username AND
1634
* whether the associated password field has the matching password.
1635
*
1636
* @note This could possibly be unified with getFieldContext but they have
1637
* slightly different use cases. getFieldContext looks up recipes whereas this
1638
* method doesn't need to since it's only returning a boolean based upon the
1639
* recipes used for the last fill (in _fillForm).
1640
*
1641
* @param {HTMLInputElement} aUsernameField element contained in a LoginForm
1642
* cached in LoginFormFactory.
1643
* @returns {Boolean} whether the username and password fields still have the
1644
* last-filled values, if previously filled.
1645
*/
1646
_isLoginAlreadyFilled(aUsernameField) {
1647
let formLikeRoot = FormLikeFactory.findRootForField(aUsernameField);
1648
// Look for the existing LoginForm.
1649
let existingLoginForm = LoginFormFactory.getForRootElement(formLikeRoot);
1650
if (!existingLoginForm) {
1651
throw new Error("_isLoginAlreadyFilled called with a username field with " +
1652
"no rootElement LoginForm");
1653
}
1654
1655
log("_isLoginAlreadyFilled: existingLoginForm", existingLoginForm);
1656
let filledLogin = this.stateForDocument(aUsernameField.ownerDocument).fillsByRootElement.get(formLikeRoot);
1657
if (!filledLogin) {
1658
return false;
1659
}
1660
1661
// Unpack the weak references.
1662
let autoFilledUsernameField = filledLogin.usernameField ? filledLogin.usernameField.get() : null;
1663
let autoFilledPasswordField = filledLogin.passwordField.get();
1664
1665
// Check username and password values match what was filled.
1666
if (!autoFilledUsernameField ||
1667
autoFilledUsernameField != aUsernameField ||
1668
autoFilledUsernameField.value != filledLogin.username ||
1669
!autoFilledPasswordField ||
1670
autoFilledPasswordField.value != filledLogin.password) {
1671
return false;
1672
}
1673
1674
return true;
1675
},
1676
1677
/**
1678
* Returns the username and password fields found in the form by input
1679
* element into form.
1680
*
1681
* @param {HTMLInputElement} aField
1682
* A form field into form.
1683
* @return {Array} [usernameField, newPasswordField, oldPasswordField]
1684
*
1685
* More detail of these values is same as _getFormFields.
1686
*/
1687
getUserNameAndPasswordFields(aField) {
1688
// If the element is not a proper form field, return null.
1689
if (ChromeUtils.getClassName(aField) !== "HTMLInputElement" ||
1690
(aField.type != "password" && !LoginHelper.isUsernameFieldType(aField)) ||
1691
aField.nodePrincipal.isNullPrincipal ||
1692
!aField.ownerDocument) {
1693
return [null, null, null];
1694
}
1695
let form = LoginFormFactory.createFromField(aField);
1696
1697
let doc = aField.ownerDocument;
1698
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
1699
let recipes = LoginRecipesContent.getRecipes(formOrigin, doc.defaultView);
1700
1701
return this._getFormFields(form, false, recipes);
1702
},
1703
1704
/**
1705
* Verify if a field is a valid login form field and
1706
* returns some information about it's LoginForm.
1707
*
1708
* @param {Element} aField
1709
* A form field we want to verify.
1710
*
1711
* @returns {Object} an object with information about the
1712
* LoginForm username and password field
1713
* or null if the passed field is invalid.
1714
*/
1715
getFieldContext(aField) {
1716
// If the element is not a proper form field, return null.
1717
if (ChromeUtils.getClassName(aField) !== "HTMLInputElement" ||
1718
(aField.type != "password" && !LoginHelper.isUsernameFieldType(aField)) ||
1719
aField.nodePrincipal.isNullPrincipal ||
1720
!aField.ownerDocument) {
1721
return null;
1722
}
1723
1724
let [usernameField, newPasswordField] = this.getUserNameAndPasswordFields(aField);
1725
1726
// If we are not verifying a password field, we want
1727
// to use aField as the username field.
1728
if (aField.type != "password") {
1729
usernameField = aField;
1730
}
1731
1732
return {
1733
usernameField: {
1734
found: !!usernameField,
1735
disabled: usernameField && (usernameField.disabled || usernameField.readOnly),
1736
},
1737
passwordField: {
1738
found: !!newPasswordField,
1739
disabled: newPasswordField && (newPasswordField.disabled || newPasswordField.readOnly),
1740
},
1741
};
1742
},
1743
};