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