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