Source code

Revision control

Other Tools

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