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