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