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
"use strict";
6
7
const { XPCOMUtils } = ChromeUtils.import(
9
);
10
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11
12
const LoginInfo = new Components.Constructor(
13
"@mozilla.org/login-manager/loginInfo;1",
14
Ci.nsILoginInfo,
15
"init"
16
);
17
18
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
19
20
ChromeUtils.defineModuleGetter(
21
this,
22
"LoginHelper",
24
);
25
ChromeUtils.defineModuleGetter(
26
this,
27
"PasswordGenerator",
29
);
30
ChromeUtils.defineModuleGetter(
31
this,
32
"PrivateBrowsingUtils",
34
);
35
36
XPCOMUtils.defineLazyServiceGetter(
37
this,
38
"prompterSvc",
39
"@mozilla.org/login-manager/prompter;1",
40
Ci.nsILoginManagerPrompter
41
);
42
43
XPCOMUtils.defineLazyGetter(this, "log", () => {
44
let logger = LoginHelper.createLogger("LoginManagerParent");
45
return logger.log.bind(logger);
46
});
47
48
const EXPORTED_SYMBOLS = ["LoginManagerParent"];
49
50
/**
51
* A listener for notifications to tests.
52
*/
53
let gListenerForTests = null;
54
55
/**
56
* A map of a principal's origin (including suffixes) to a generated password string and filled flag
57
* so that we can offer the same password later (e.g. in a confirmation field).
58
*
59
* We don't currently evict from this cache so entries should last until the end of the browser
60
* session. That may change later but for now a typical session would max out at a few entries.
61
*/
62
let gGeneratedPasswordsByPrincipalOrigin = new Map();
63
64
/**
65
* Reference to the default LoginRecipesParent (instead of the initialization promise) for
66
* synchronous access. This is a temporary hack and new consumers should yield on
67
* recipeParentPromise instead.
68
*
69
* @type LoginRecipesParent
70
* @deprecated
71
*/
72
let gRecipeManager = null;
73
74
/**
75
* Tracks the last time the user cancelled the master password prompt,
76
* to avoid spamming master password prompts on autocomplete searches.
77
* TODO: Bug XXX - Should be `Number.NEGATIVE_INFINITY`.
78
*/
79
let gLastMPLoginCancelled = Math.NEGATIVE_INFINITY;
80
81
let gGeneratedPasswordObserver = {
82
addedObserver: false,
83
84
observe(subject, topic, data) {
85
if (topic == "last-pb-context-exited") {
86
// The last private browsing context closed so clear all cached generated
87
// passwords for private window origins.
88
for (let principalOrigin of gGeneratedPasswordsByPrincipalOrigin.keys()) {
89
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
90
principalOrigin
91
);
92
if (!principal.privateBrowsingId) {
93
// The origin isn't for a private context so leave it alone.
94
continue;
95
}
96
gGeneratedPasswordsByPrincipalOrigin.delete(principalOrigin);
97
}
98
return;
99
}
100
101
if (
102
topic == "passwordmgr-autosaved-login-merged" ||
103
(topic == "passwordmgr-storage-changed" && data == "removeLogin")
104
) {
105
let { origin, guid } = subject;
106
let generatedPW = gGeneratedPasswordsByPrincipalOrigin.get(origin);
107
108
// in the case where an autosaved login removed or merged into an existing login,
109
// clear the guid associated with the generated-password cache entry
110
if (
111
generatedPW &&
112
(guid == generatedPW.storageGUID ||
113
topic == "passwordmgr-autosaved-login-merged")
114
) {
115
log(
116
"Removing storageGUID for generated-password cache entry on origin:",
117
origin
118
);
119
generatedPW.storageGUID = null;
120
}
121
}
122
},
123
};
124
125
Services.ppmm.addMessageListener("PasswordManager:findRecipes", message => {
126
let formHost = new URL(message.data.formOrigin).host;
127
return gRecipeManager.getRecipesForHost(formHost);
128
});
129
130
class LoginManagerParent extends JSWindowActorParent {
131
// This is used by tests to listen to form submission.
132
static setListenerForTests(listener) {
133
gListenerForTests = listener;
134
}
135
136
// Used by tests to clean up recipes only when they were actually used.
137
static get _recipeManager() {
138
return gRecipeManager;
139
}
140
141
// Some unit tests need to access this.
142
static getGeneratedPasswordsByPrincipalOrigin() {
143
return gGeneratedPasswordsByPrincipalOrigin;
144
}
145
146
getRootBrowser() {
147
let browsingContext = null;
148
if (this._overrideBrowsingContextId) {
149
browsingContext = BrowsingContext.get(this._overrideBrowsingContextId);
150
} else {
151
browsingContext = this.browsingContext.top;
152
}
153
return browsingContext.embedderElement;
154
}
155
156
/**
157
* @param {origin} formOrigin
158
* @param {object} options
159
* @param {origin?} options.formActionOrigin To match on. Omit this argument to match all action origins.
160
* @param {origin?} options.httpRealm To match on. Omit this argument to match all realms.
161
* @param {boolean} options.acceptDifferentSubdomains Include results for eTLD+1 matches
162
* @param {boolean} options.ignoreActionAndRealm Include all form and HTTP auth logins for the site
163
*/
164
static async searchAndDedupeLogins(
165
formOrigin,
166
{
167
acceptDifferentSubdomains,
168
formActionOrigin,
169
httpRealm,
170
ignoreActionAndRealm,
171
} = {}
172
) {
173
let logins;
174
let matchData = {
175
origin: formOrigin,
176
schemeUpgrades: LoginHelper.schemeUpgrades,
177
acceptDifferentSubdomains,
178
};
179
if (!ignoreActionAndRealm) {
180
if (typeof formActionOrigin != "undefined") {
181
matchData.formActionOrigin = formActionOrigin;
182
} else if (typeof httpRealm != "undefined") {
183
matchData.httpRealm = httpRealm;
184
}
185
}
186
try {
187
logins = await Services.logins.searchLoginsAsync(matchData);
188
} catch (e) {
189
// Record the last time the user cancelled the MP prompt
190
// to avoid spamming them with MP prompts for autocomplete.
191
if (e.result == Cr.NS_ERROR_ABORT) {
192
log("User cancelled master password prompt.");
193
gLastMPLoginCancelled = Date.now();
194
return [];
195
}
196
throw e;
197
}
198
199
logins = LoginHelper.shadowHTTPLogins(logins);
200
201
let resolveBy = [
202
"subdomain",
203
"actionOrigin",
204
"scheme",
205
"timePasswordChanged",
206
];
207
return LoginHelper.dedupeLogins(
208
logins,
209
["username", "password"],
210
resolveBy,
211
formOrigin,
212
formActionOrigin
213
);
214
}
215
216
receiveMessage(msg) {
217
let data = msg.data;
218
switch (msg.name) {
219
case "PasswordManager:findLogins": {
220
// TODO Verify the target's principals against the formOrigin?
221
return this.sendLoginDataToChild(
222
data.formOrigin,
223
data.actionOrigin,
224
data.options
225
);
226
}
227
228
case "PasswordManager:onFormSubmit": {
229
// TODO Verify msg.target's principals against the formOrigin?
230
let browser = this.getRootBrowser();
231
let submitPromise = this.onFormSubmit(browser, data);
232
if (gListenerForTests) {
233
submitPromise.then(() => {
234
gListenerForTests("FormSubmit", data);
235
});
236
}
237
break;
238
}
239
240
case "PasswordManager:onPasswordEditedOrGenerated": {
241
log("Received PasswordManager:onPasswordEditedOrGenerated");
242
if (gListenerForTests) {
243
log("calling gListenerForTests");
244
gListenerForTests("PasswordEditedOrGenerated", {});
245
}
246
let browser = this.getRootBrowser();
247
this._onPasswordEditedOrGenerated(browser, data);
248
break;
249
}
250
251
case "PasswordManager:autoCompleteLogins": {
252
return this.doAutocompleteSearch(data);
253
}
254
255
case "PasswordManager:removeLogin": {
256
let login = LoginHelper.vanillaObjectToLogin(data.login);
257
Services.logins.removeLogin(login);
258
break;
259
}
260
261
case "PasswordManager:OpenPreferences": {
262
let window = this.getRootBrowser().ownerGlobal;
263
LoginHelper.openPasswordManager(window, {
264
filterString: msg.data.hostname,
265
entryPoint: msg.data.entryPoint,
266
});
267
break;
268
}
269
270
// Used by tests to detect that a form-fill has occurred. This redirects
271
// to the top-level browsing context.
272
case "PasswordManager:formProcessed": {
273
let topActor = this.browsingContext.top.currentWindowGlobal.getActor(
274
"LoginManager"
275
);
276
topActor.sendAsyncMessage("PasswordManager:formProcessed", {
277
formid: data.formid,
278
});
279
if (gListenerForTests) {
280
gListenerForTests("FormProcessed", {
281
browsingContext: this.browsingContext,
282
});
283
}
284
break;
285
}
286
}
287
288
return undefined;
289
}
290
291
/**
292
* Trigger a login form fill and send relevant data (e.g. logins and recipes)
293
* to the child process (LoginManagerChild).
294
*/
295
async fillForm({ browser, loginFormOrigin, login, inputElementIdentifier }) {
296
let recipes = [];
297
if (loginFormOrigin) {
298
let formHost;
299
try {
300
formHost = new URL(loginFormOrigin).host;
301
let recipeManager = await LoginManagerParent.recipeParentPromise;
302
recipes = recipeManager.getRecipesForHost(formHost);
303
} catch (ex) {
304
// Some schemes e.g. chrome aren't supported by URL
305
}
306
}
307
308
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
309
// doesn't support structured cloning.
310
let jsLogins = [LoginHelper.loginToVanillaObject(login)];
311
312
let browserURI = browser.currentURI.spec;
313
let originMatches =
314
LoginHelper.getLoginOrigin(browserURI) == loginFormOrigin;
315
316
this.sendAsyncMessage("PasswordManager:fillForm", {
317
inputElementIdentifier,
318
loginFormOrigin,
319
originMatches,
320
logins: jsLogins,
321
recipes,
322
});
323
}
324
325
/**
326
* Send relevant data (e.g. logins and recipes) to the child process (LoginManagerChild).
327
*/
328
async sendLoginDataToChild(
329
formOrigin,
330
actionOrigin,
331
{ guid, showMasterPassword }
332
) {
333
let recipes = [];
334
let formHost;
335
try {
336
formHost = new URL(formOrigin).host;
337
let recipeManager = await LoginManagerParent.recipeParentPromise;
338
recipes = recipeManager.getRecipesForHost(formHost);
339
} catch (ex) {
340
// Some schemes e.g. chrome aren't supported by URL
341
}
342
343
if (!showMasterPassword && !Services.logins.isLoggedIn) {
344
return { logins: [], recipes };
345
}
346
347
// If we're currently displaying a master password prompt, defer
348
// processing this form until the user handles the prompt.
349
if (Services.logins.uiBusy) {
350
log("deferring sendLoginDataToChild for", formOrigin);
351
352
let uiBusyPromiseResolve;
353
let uiBusyPromise = new Promise(resolve => {
354
uiBusyPromiseResolve = resolve;
355
});
356
357
let self = this;
358
let observer = {
359
QueryInterface: ChromeUtils.generateQI([
360
Ci.nsIObserver,
361
Ci.nsISupportsWeakReference,
362
]),
363
364
observe(subject, topic, data) {
365
log("Got deferred sendLoginDataToChild notification:", topic);
366
// Only run observer once.
367
Services.obs.removeObserver(this, "passwordmgr-crypto-login");
368
Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
369
if (topic == "passwordmgr-crypto-loginCanceled") {
370
uiBusyPromiseResolve({ logins: [], recipes });
371
return;
372
}
373
374
let result = self.sendLoginDataToChild(formOrigin, actionOrigin, {
375
showMasterPassword,
376
});
377
uiBusyPromiseResolve(result);
378
},
379
};
380
381
// Possible leak: it's possible that neither of these notifications
382
// will fire, and if that happens, we'll leak the observer (and
383
// never return). We should guarantee that at least one of these
384
// will fire.
385
// See bug XXX.
386
Services.obs.addObserver(observer, "passwordmgr-crypto-login");
387
Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled");
388
389
return uiBusyPromise;
390
}
391
392
// Autocomplete results do not need to match actionOrigin or exact origin.
393
let logins = null;
394
if (guid) {
395
logins = await Services.logins.searchLoginsAsync({
396
guid,
397
origin: formOrigin,
398
});
399
} else {
400
logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, {
401
formActionOrigin: actionOrigin,
402
ignoreActionAndRealm: true,
403
acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup,
404
});
405
}
406
407
log("sendLoginDataToChild:", logins.length, "deduped logins");
408
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
409
// doesn't support structured cloning.
410
let jsLogins = LoginHelper.loginsToVanillaObjects(logins);
411
return { logins: jsLogins, recipes };
412
}
413
414
async doAutocompleteSearch({
415
formOrigin,
416
actionOrigin,
417
searchString,
418
previousResult,
419
forcePasswordGeneration,
420
hasBeenTypePassword,
421
isSecure,
422
isProbablyANewPasswordField,
423
}) {
424
// Note: previousResult is a regular object, not an
425
// nsIAutoCompleteResult.
426
427
// Cancel if the master password prompt is already showing or we unsuccessfully prompted for it too recently.
428
if (!Services.logins.isLoggedIn) {
429
if (Services.logins.uiBusy) {
430
log(
431
"Not searching logins for autocomplete since the master password prompt is already showing"
432
);
433
// Return an empty array to make LoginManagerChild clear the
434
// outstanding request it has temporarily saved.
435
return { logins: [] };
436
}
437
438
let timeDiff = Date.now() - gLastMPLoginCancelled;
439
if (timeDiff < LoginManagerParent._repromptTimeout) {
440
log(
441
"Not searching logins for autocomplete since the master password " +
442
`prompt was last cancelled ${Math.round(
443
timeDiff / 1000
444
)} seconds ago.`
445
);
446
// Return an empty array to make LoginManagerChild clear the
447
// outstanding request it has temporarily saved.
448
return { logins: [] };
449
}
450
}
451
452
let searchStringLower = searchString.toLowerCase();
453
let logins;
454
if (
455
previousResult &&
456
searchStringLower.startsWith(previousResult.searchString.toLowerCase())
457
) {
458
log("Using previous autocomplete result");
459
460
// We have a list of results for a shorter search string, so just
461
// filter them further based on the new search string.
462
logins = LoginHelper.vanillaObjectsToLogins(previousResult.logins);
463
} else {
464
log("Creating new autocomplete search result.");
465
466
// Autocomplete results do not need to match actionOrigin or exact origin.
467
logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, {
468
formActionOrigin: actionOrigin,
469
ignoreActionAndRealm: true,
470
acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup,
471
});
472
}
473
474
let matchingLogins = logins.filter(function(fullMatch) {
475
let match = fullMatch.username;
476
477
// Remove results that are too short, or have different prefix.
478
// Also don't offer empty usernames as possible results except
479
// for on password fields.
480
if (hasBeenTypePassword) {
481
return true;
482
}
483
return match && match.toLowerCase().startsWith(searchStringLower);
484
});
485
486
let generatedPassword = null;
487
let willAutoSaveGeneratedPassword = false;
488
if (
489
// If MP was cancelled above, don't try to offer pwgen or access storage again (causing a new MP prompt).
490
Services.logins.isLoggedIn &&
491
(forcePasswordGeneration ||
492
(isProbablyANewPasswordField &&
493
Services.logins.getLoginSavingEnabled(formOrigin)))
494
) {
495
generatedPassword = this.getGeneratedPassword();
496
let potentialConflictingLogins = LoginHelper.searchLoginsWithObject({
497
origin: formOrigin,
498
formActionOrigin: actionOrigin,
499
httpRealm: null,
500
});
501
willAutoSaveGeneratedPassword = !potentialConflictingLogins.find(
502
login => login.username == ""
503
);
504
}
505
506
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
507
// doesn't support structured cloning.
508
let jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
509
return {
510
generatedPassword,
511
logins: jsLogins,
512
willAutoSaveGeneratedPassword,
513
};
514
}
515
516
/**
517
* Expose `BrowsingContext` so we can stub it in tests.
518
*/
519
static get _browsingContextGlobal() {
520
return BrowsingContext;
521
}
522
523
// Set an override context within a test.
524
useBrowsingContext(browsingContextId = 0) {
525
this._overrideBrowsingContextId = browsingContextId;
526
}
527
528
getBrowsingContextToUse() {
529
if (this._overrideBrowsingContextId) {
530
return BrowsingContext.get(this._overrideBrowsingContextId);
531
}
532
533
return this.browsingContext;
534
}
535
536
getGeneratedPassword() {
537
if (
538
!LoginHelper.enabled ||
539
!LoginHelper.generationAvailable ||
540
!LoginHelper.generationEnabled
541
) {
542
return null;
543
}
544
545
let browsingContext = this.getBrowsingContextToUse();
546
if (!browsingContext) {
547
return null;
548
}
549
let framePrincipalOrigin =
550
browsingContext.currentWindowGlobal.documentPrincipal.origin;
551
// Use the same password if we already generated one for this origin so that it doesn't change
552
// with each search/keystroke and the user can easily re-enter a password in a confirmation field.
553
let generatedPW = gGeneratedPasswordsByPrincipalOrigin.get(
554
framePrincipalOrigin
555
);
556
if (generatedPW) {
557
return generatedPW.value;
558
}
559
560
generatedPW = {
561
autocompleteShown: false,
562
edited: false,
563
filled: false,
564
/**
565
* GUID of a login that was already saved for this generated password that
566
* will be automatically updated with password changes. This shouldn't be
567
* an existing saved login for the site unless the user chose to
568
* merge/overwrite via a doorhanger.
569
*/
570
storageGUID: null,
571
value: PasswordGenerator.generatePassword(),
572
};
573
574
// Add these observers when a password is assigned.
575
if (!gGeneratedPasswordObserver.addedObserver) {
576
Services.obs.addObserver(
577
gGeneratedPasswordObserver,
578
"passwordmgr-autosaved-login-merged"
579
);
580
Services.obs.addObserver(
581
gGeneratedPasswordObserver,
582
"passwordmgr-storage-changed"
583
);
584
Services.obs.addObserver(
585
gGeneratedPasswordObserver,
586
"last-pb-context-exited"
587
);
588
gGeneratedPasswordObserver.addedObserver = true;
589
}
590
591
gGeneratedPasswordsByPrincipalOrigin.set(framePrincipalOrigin, generatedPW);
592
return generatedPW.value;
593
}
594
595
maybeRecordPasswordGenerationShownTelemetryEvent(autocompleteResults) {
596
if (!autocompleteResults.some(r => r.style == "generatedPassword")) {
597
return;
598
}
599
600
let browsingContext = this.getBrowsingContextToUse();
601
602
let framePrincipalOrigin =
603
browsingContext.currentWindowGlobal.documentPrincipal.origin;
604
let generatedPW = gGeneratedPasswordsByPrincipalOrigin.get(
605
framePrincipalOrigin
606
);
607
608
// We only want to record the first time it was shown for an origin
609
if (generatedPW.autocompleteShown) {
610
return;
611
}
612
613
generatedPW.autocompleteShown = true;
614
615
Services.telemetry.recordEvent(
616
"pwmgr",
617
"autocomplete_shown",
618
"generatedpassword"
619
);
620
}
621
622
/**
623
* Used for stubbing by tests.
624
*/
625
_getPrompter() {
626
return prompterSvc;
627
}
628
629
async onFormSubmit(
630
browser,
631
{
632
origin,
633
browsingContextId,
634
formActionOrigin,
635
autoFilledLoginGuid,
636
usernameField,
637
newPasswordField,
638
oldPasswordField,
639
dismissedPrompt,
640
}
641
) {
642
function recordLoginUse(login) {
643
if (!browser || PrivateBrowsingUtils.isBrowserPrivate(browser)) {
644
// don't record non-interactive use in private browsing
645
return;
646
}
647
Services.logins.recordPasswordUse(login);
648
}
649
650
// If password storage is disabled, bail out.
651
if (!LoginHelper.storageEnabled) {
652
return;
653
}
654
655
if (!Services.logins.getLoginSavingEnabled(origin)) {
656
log("(form submission ignored -- saving is disabled for:", origin, ")");
657
return;
658
}
659
660
let browsingContext = BrowsingContext.get(browsingContextId);
661
let framePrincipalOrigin =
662
browsingContext.currentWindowGlobal.documentPrincipal.origin;
663
log("onFormSubmit, got framePrincipalOrigin: ", framePrincipalOrigin);
664
665
let formLogin = new LoginInfo(
666
origin,
667
formActionOrigin,
668
null,
669
usernameField ? usernameField.value : "",
670
newPasswordField.value,
671
usernameField ? usernameField.name : "",
672
newPasswordField.name
673
);
674
// we don't auto-save logins on form submit
675
let notifySaved = false;
676
677
if (autoFilledLoginGuid) {
678
let loginsForGuid = await Services.logins.searchLoginsAsync({
679
guid: autoFilledLoginGuid,
680
origin,
681
});
682
if (
683
loginsForGuid.length == 1 &&
684
loginsForGuid[0].password == formLogin.password &&
685
(!formLogin.username || // Also cover cases where only the password is requested.
686
loginsForGuid[0].username == formLogin.username)
687
) {
688
log("The filled login matches the form submission. Nothing to change.");
689
recordLoginUse(loginsForGuid[0]);
690
return;
691
}
692
}
693
694
let existingLogin = null;
695
let canMatchExistingLogin = true;
696
// Below here we have one login per hostPort + action + username with the
697
// matching scheme being preferred.
698
let logins = await LoginManagerParent.searchAndDedupeLogins(origin, {
699
formActionOrigin,
700
});
701
702
let generatedPW = gGeneratedPasswordsByPrincipalOrigin.get(
703
framePrincipalOrigin
704
);
705
let autoSavedStorageGUID = "";
706
if (generatedPW && generatedPW.storageGUID) {
707
autoSavedStorageGUID = generatedPW.storageGUID;
708
}
709
710
// If we didn't find a username field, but seem to be changing a
711
// password, allow the user to select from a list of applicable
712
// logins to update the password for.
713
if (!usernameField && oldPasswordField && logins.length) {
714
if (logins.length == 1) {
715
existingLogin = logins[0];
716
717
if (existingLogin.password == formLogin.password) {
718
recordLoginUse(existingLogin);
719
log(
720
"(Not prompting to save/change since we have no username and the " +
721
"only saved password matches the new password)"
722
);
723
return;
724
}
725
726
formLogin.username = existingLogin.username;
727
formLogin.usernameField = existingLogin.usernameField;
728
} else if (!generatedPW || generatedPW.value != newPasswordField.value) {
729
// Note: It's possible that that we already have the correct u+p saved
730
// but since we don't have the username, we don't know if the user is
731
// changing a second account to the new password so we ask anyways.
732
canMatchExistingLogin = false;
733
}
734
}
735
736
if (canMatchExistingLogin && !existingLogin) {
737
// Look for an existing login that matches the form login.
738
for (let login of logins) {
739
let same;
740
741
// If one login has a username but the other doesn't, ignore
742
// the username when comparing and only match if they have the
743
// same password. Otherwise, compare the logins and match even
744
// if the passwords differ.
745
if (!login.username && formLogin.username) {
746
let restoreMe = formLogin.username;
747
formLogin.username = "";
748
same = LoginHelper.doLoginsMatch(formLogin, login, {
749
ignorePassword: false,
750
ignoreSchemes: LoginHelper.schemeUpgrades,
751
});
752
formLogin.username = restoreMe;
753
} else if (!formLogin.username && login.username) {
754
formLogin.username = login.username;
755
same = LoginHelper.doLoginsMatch(formLogin, login, {
756
ignorePassword: false,
757
ignoreSchemes: LoginHelper.schemeUpgrades,
758
});
759
formLogin.username = ""; // we know it's always blank.
760
} else {
761
same = LoginHelper.doLoginsMatch(formLogin, login, {
762
ignorePassword: true,
763
ignoreSchemes: LoginHelper.schemeUpgrades,
764
});
765
}
766
767
if (same) {
768
existingLogin = login;
769
break;
770
}
771
}
772
}
773
774
let promptBrowser = LoginHelper.getBrowserForPrompt(browser);
775
let prompter = this._getPrompter(browser);
776
777
if (!canMatchExistingLogin) {
778
prompter.promptToChangePasswordWithUsernames(
779
promptBrowser,
780
logins,
781
formLogin
782
);
783
return;
784
}
785
if (existingLogin) {
786
log("Found an existing login matching this form submission");
787
788
// Change password if needed.
789
if (existingLogin.password != formLogin.password) {
790
log("...passwords differ, prompting to change.");
791
prompter.promptToChangePassword(
792
promptBrowser,
793
existingLogin,
794
formLogin,
795
dismissedPrompt,
796
notifySaved,
797
autoSavedStorageGUID,
798
autoFilledLoginGuid
799
);
800
} else if (!existingLogin.username && formLogin.username) {
801
log("...empty username update, prompting to change.");
802
let prompter = this._getPrompter(browser);
803
prompter.promptToChangePassword(
804
promptBrowser,
805
existingLogin,
806
formLogin,
807
dismissedPrompt,
808
notifySaved,
809
autoSavedStorageGUID,
810
autoFilledLoginGuid
811
);
812
} else {
813
recordLoginUse(existingLogin);
814
}
815
816
return;
817
}
818
819
// Prompt user to save login (via dialog or notification bar)
820
prompter.promptToSavePassword(
821
promptBrowser,
822
formLogin,
823
dismissedPrompt,
824
notifySaved,
825
autoFilledLoginGuid
826
);
827
}
828
829
async _onPasswordEditedOrGenerated(
830
browser,
831
{
832
origin,
833
formActionOrigin,
834
autoFilledLoginGuid,
835
newPasswordField,
836
usernameField = null,
837
oldPasswordField,
838
triggeredByFillingGenerated = false,
839
}
840
) {
841
log(
842
"_onPasswordEditedOrGenerated, triggeredByFillingGenerated:",
843
triggeredByFillingGenerated
844
);
845
846
// If password storage is disabled, bail out.
847
if (!LoginHelper.storageEnabled) {
848
return;
849
}
850
851
if (!Services.logins.getLoginSavingEnabled(origin)) {
852
// No UI should be shown to offer generation in this case but a user may
853
// disable saving for the site after already filling one and they may then
854
// edit it.
855
log("_onPasswordEditedOrGenerated: saving is disabled for:", origin);
856
return;
857
}
858
859
if (!newPasswordField.value) {
860
log("_onPasswordEditedOrGenerated: The password field is empty");
861
return;
862
}
863
864
if (!browser) {
865
log("_onPasswordEditedOrGenerated: The browser is gone");
866
return;
867
}
868
869
let browsingContext = this.getBrowsingContextToUse();
870
if (!browsingContext) {
871
return;
872
}
873
874
let framePrincipalOrigin =
875
browsingContext.currentWindowGlobal.documentPrincipal.origin;
876
log(
877
"_onPasswordEditedOrGenerated: got framePrincipalOrigin: ",
878
framePrincipalOrigin
879
);
880
881
let {
882
originNoSuffix,
883
} = browsingContext.currentWindowGlobal.documentPrincipal;
884
let formOrigin = LoginHelper.getLoginOrigin(originNoSuffix);
885
if (formOrigin !== origin) {
886
log(
887
"_onPasswordEditedOrGenerated: Invalid form origin:",
888
browsingContext.currentWindowGlobal.documentPrincipal
889
);
890
return;
891
}
892
893
let formLogin = new LoginInfo(
894
origin,
895
formActionOrigin,
896
null,
897
usernameField ? usernameField.value : "",
898
newPasswordField.value,
899
usernameField ? usernameField.name : "",
900
newPasswordField.name
901
);
902
let existingLogin = null;
903
let canMatchExistingLogin = true;
904
let shouldAutoSaveLogin = triggeredByFillingGenerated;
905
let autoSavedLogin = null;
906
let notifySaved = false;
907
908
if (autoFilledLoginGuid) {
909
let [matchedLogin] = await Services.logins.searchLoginsAsync({
910
guid: autoFilledLoginGuid,
911
origin,
912
});
913
if (
914
matchedLogin &&
915
matchedLogin.password == formLogin.password &&
916
(!formLogin.username || // Also cover cases where only the password is requested.
917
matchedLogin.username == formLogin.username)
918
) {
919
log("The filled login matches the changed fields. Nothing to change.");
920
// We may want to update an existing doorhanger
921
existingLogin = matchedLogin;
922
}
923
}
924
925
let generatedPW = gGeneratedPasswordsByPrincipalOrigin.get(
926
framePrincipalOrigin
927
);
928
929
// Below here we have one login per hostPort + action + username with the
930
// matching scheme being preferred.
931
let logins = await LoginManagerParent.searchAndDedupeLogins(origin, {
932
formActionOrigin,
933
});
934
// only used in the generated pw case where we auto-save
935
let formLoginWithoutUsername;
936
937
if (triggeredByFillingGenerated && generatedPW) {
938
log("Got cached generatedPW");
939
formLoginWithoutUsername = new LoginInfo(
940
formOrigin,
941
formActionOrigin,
942
null,
943
"",
944
newPasswordField.value
945
);
946
947
if (newPasswordField.value != generatedPW.value) {
948
// The user edited the field after generation to a non-empty value.
949
log("The field containing the generated password has changed");
950
951
// Record telemetry for the first edit
952
if (!generatedPW.edited) {
953
Services.telemetry.recordEvent(
954
"pwmgr",
955
"filled_field_edited",
956
"generatedpassword"
957
);
958
log("filled_field_edited telemetry event recorded");
959
generatedPW.edited = true;
960
}
961
}
962
963
// This will throw if we can't look up the entry in the password/origin map
964
if (!generatedPW.filled) {
965
if (generatedPW.storageGUID) {
966
throw new Error(
967
"Generated password was saved in storage without being filled first"
968
);
969
}
970
// record first use of this generated password
971
Services.telemetry.recordEvent(
972
"pwmgr",
973
"autocomplete_field",
974
"generatedpassword"
975
);
976
log("autocomplete_field telemetry event recorded");
977
generatedPW.filled = true;
978
}
979
980
// We may have already autosaved this login
981
// Note that it could have been saved in a totally different tab in the session.
982
if (generatedPW.storageGUID) {
983
[autoSavedLogin] = await Services.logins.searchLoginsAsync({
984
guid: generatedPW.storageGUID,
985
origin: formOrigin,
986
});
987
988
if (autoSavedLogin) {
989
log(
990
"_onPasswordEditedOrGenerated: login to change is the auto-saved login"
991
);
992
existingLogin = autoSavedLogin;
993
}
994
// The generated password login may have been deleted in the meantime.
995
// Proceed to maybe save a new login below.
996
}
997
generatedPW.value = newPasswordField.value;
998
999
if (!existingLogin) {
1000
log(
1001
"_onPasswordEditedOrGenerated: Didnt match generated-password login"
1002
);
1003
1004
// Check if we already have a login saved for this site since we don't want to overwrite it in
1005
// case the user still needs their old password to successfully complete a password change.
1006
let matchedLogin = logins.find(login =>
1007
formLoginWithoutUsername.matches(login, true)
1008
);
1009
if (matchedLogin) {
1010
shouldAutoSaveLogin = false;
1011
if (matchedLogin.password == formLoginWithoutUsername.password) {
1012
// This login is already saved so show no new UI.
1013
// We may want to update an existing doorhanger though...
1014
log("_onPasswordEditedOrGenerated: Matching login already saved");
1015
existingLogin = matchedLogin;
1016
}
1017
log(
1018
"_onPasswordEditedOrGenerated: Login with empty username already saved for this site"
1019
);
1020
}
1021
}
1022
}
1023
1024
// If we didn't find a username field, but seem to be changing a
1025
// password, use the first match if there is only one
1026
// If there's more than one we'll prompt to save with the initial formLogin
1027
// and let the doorhanger code resolve this
1028
if (
1029
!triggeredByFillingGenerated &&
1030
!existingLogin &&
1031
!usernameField &&
1032
oldPasswordField &&
1033
logins.length
1034
) {
1035
if (logins.length == 1) {
1036
existingLogin = logins[0];
1037
1038
if (existingLogin.password == formLogin.password) {
1039
log(
1040
"(Not prompting to save/change since we have no username and the " +
1041
"only saved password matches the new password)"
1042
);
1043
return;
1044
}
1045
1046
formLogin.username = existingLogin.username;
1047
formLogin.usernameField = existingLogin.usernameField;
1048
} else if (!generatedPW || generatedPW.value != newPasswordField.value) {
1049
// Note: It's possible that that we already have the correct u+p saved
1050
// but since we don't have the username, we don't know if the user is
1051
// changing a second account to the new password so we ask anyways.
1052
canMatchExistingLogin = false;
1053
}
1054
}
1055
1056
if (canMatchExistingLogin && !existingLogin) {
1057
// Look for an existing login that matches the form login.
1058
for (let login of logins) {
1059
let same;
1060
1061
// If one login has a username but the other doesn't, ignore
1062
// the username when comparing and only match if they have the
1063
// same password. Otherwise, compare the logins and match even
1064
// if the passwords differ.
1065
if (!login.username && formLogin.username) {
1066
let restoreMe = formLogin.username;
1067
formLogin.username = "";
1068
same = LoginHelper.doLoginsMatch(formLogin, login, {
1069
ignorePassword: false,
1070
ignoreSchemes: LoginHelper.schemeUpgrades,
1071
});
1072
formLogin.username = restoreMe;
1073
} else if (!formLogin.username && login.username) {
1074
formLogin.username = login.username;
1075
same = LoginHelper.doLoginsMatch(formLogin, login, {
1076
ignorePassword: false,
1077
ignoreSchemes: LoginHelper.schemeUpgrades,
1078
});
1079
formLogin.username = ""; // we know it's always blank.
1080
} else {
1081
same = LoginHelper.doLoginsMatch(formLogin, login, {
1082
ignorePassword: true,
1083
ignoreSchemes: LoginHelper.schemeUpgrades,
1084
});
1085
}
1086
1087
if (same) {
1088
existingLogin = login;
1089
log("_onPasswordEditedOrGenerated: matched saved login");
1090
break;
1091
}
1092
}
1093
}
1094
1095
if (shouldAutoSaveLogin) {
1096
if (
1097
existingLogin &&
1098
existingLogin == autoSavedLogin &&
1099
existingLogin.password !== formLogin.password
1100
) {
1101
log("_onPasswordEditedOrGenerated: updating auto-saved login");
1102
1103
Services.logins.modifyLogin(
1104
existingLogin,
1105
LoginHelper.newPropertyBag({
1106
password: formLogin.password,
1107
})
1108
);
1109
notifySaved = true;
1110
// Update `existingLogin` with the new password if modifyLogin didn't
1111
// throw so that the prompts later uses the new password.
1112
existingLogin.password = formLogin.password;
1113
} else if (!autoSavedLogin) {
1114
log(
1115
"_onPasswordEditedOrGenerated: auto-saving new login with empty username"
1116
);
1117
existingLogin = Services.logins.addLogin(formLoginWithoutUsername);
1118
// Remember the GUID where we saved the generated password so we can update
1119
// the login if the user later edits the generated password.
1120
generatedPW.storageGUID = existingLogin.guid;
1121
notifySaved = true;
1122
}
1123
} else {
1124
log("_onPasswordEditedOrGenerated: not auto-saving this login");
1125
}
1126
1127
let prompter = this._getPrompter(browser);
1128
let promptBrowser = LoginHelper.getBrowserForPrompt(browser);
1129
1130
if (existingLogin) {
1131
// Show a change doorhanger to allow modifying an already-saved login
1132
// e.g. to add a username or update the password.
1133
let autoSavedStorageGUID = "";
1134
if (
1135
generatedPW &&
1136
generatedPW.value == existingLogin.password &&
1137
generatedPW.storageGUID == existingLogin.guid
1138
) {
1139
autoSavedStorageGUID = generatedPW.storageGUID;
1140
}
1141
1142
// Change password if needed.
1143
if (
1144
(shouldAutoSaveLogin && !formLogin.username) ||
1145
existingLogin.password != formLogin.password
1146
) {
1147
log(
1148
"_onPasswordEditedOrGenerated: promptToChangePassword with autoSavedStorageGUID: " +
1149
autoSavedStorageGUID
1150
);
1151
prompter.promptToChangePassword(
1152
promptBrowser,
1153
existingLogin,
1154
formLogin,
1155
true, // dismissed prompt
1156
notifySaved,
1157
autoSavedStorageGUID, // autoSavedLoginGuid
1158
autoFilledLoginGuid
1159
);
1160
} else if (!existingLogin.username && formLogin.username) {
1161
log("...empty username update, prompting to change.");
1162
prompter.promptToChangePassword(
1163
promptBrowser,
1164
existingLogin,
1165
formLogin,
1166
true, // dismissed prompt
1167
notifySaved,
1168
autoSavedStorageGUID, // autoSavedLoginGuid
1169
autoFilledLoginGuid
1170
);
1171
} else {
1172
log("_onPasswordEditedOrGenerated: No change to existing login");
1173
// is there a doorhanger we should update?
1174
let popupNotifications = promptBrowser.ownerGlobal.PopupNotifications;
1175
let notif = popupNotifications.getNotification("password", browser);
1176
log(
1177
"_onPasswordEditedOrGenerated: Has doorhanger?",
1178
notif && notif.dismissed
1179
);
1180
if (notif && notif.dismissed) {
1181
prompter.promptToChangePassword(
1182
promptBrowser,
1183
existingLogin,
1184
formLogin,
1185
true, // dismissed prompt
1186
notifySaved,
1187
autoSavedStorageGUID, // autoSavedLoginGuid
1188
autoFilledLoginGuid
1189
);
1190
}
1191
}
1192
return;
1193
}
1194
log("_onPasswordEditedOrGenerated: no matching login to save/update");
1195
prompter.promptToSavePassword(
1196
promptBrowser,
1197
formLogin,
1198
true, // dismissed prompt
1199
notifySaved,
1200
autoFilledLoginGuid
1201
);
1202
}
1203
1204
static get recipeParentPromise() {
1205
if (!gRecipeManager) {
1206
const { LoginRecipesParent } = ChromeUtils.import(
1208
);
1209
gRecipeManager = new LoginRecipesParent({
1210
defaults: Services.prefs.getStringPref("signon.recipes.path"),
1211
});
1212
}
1213
1214
return gRecipeManager.initializationPromise;
1215
}
1216
}
1217
1218
XPCOMUtils.defineLazyPreferenceGetter(
1219
LoginManagerParent,
1220
"_repromptTimeout",
1221
"signon.masterPasswordReprompt.timeout_ms",
1222
900000
1223
); // 15 Minutes