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
* Contains functions shared by different Login Manager components.
7
*
8
* This JavaScript module exists in order to share code between the different
9
* XPCOM components that constitute the Login Manager, including implementations
10
* of nsILoginManager and nsILoginManagerStorage.
11
*/
12
13
"use strict";
14
15
const EXPORTED_SYMBOLS = ["LoginHelper"];
16
17
// Globals
18
19
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
20
const { XPCOMUtils } = ChromeUtils.import(
22
);
23
24
/**
25
* Contains functions shared by different Login Manager components.
26
*/
27
this.LoginHelper = {
28
debug: null,
29
enabled: null,
30
storageEnabled: null,
31
formlessCaptureEnabled: null,
32
generationAvailable: null,
33
generationEnabled: null,
34
includeOtherSubdomainsInLookup: null,
35
insecureAutofill: null,
36
managementURI: null,
37
privateBrowsingCaptureEnabled: null,
38
schemeUpgrades: null,
39
showAutoCompleteFooter: null,
40
41
init() {
42
// Watch for pref changes to update cached pref values.
43
Services.prefs.addObserver("signon.", () => this.updateSignonPrefs());
44
this.updateSignonPrefs();
45
Services.telemetry.setEventRecordingEnabled("pwmgr", true);
46
},
47
48
updateSignonPrefs() {
49
this.autofillForms = Services.prefs.getBoolPref("signon.autofillForms");
50
this.autofillAutocompleteOff = Services.prefs.getBoolPref(
51
"signon.autofillForms.autocompleteOff"
52
);
53
this.debug = Services.prefs.getBoolPref("signon.debug");
54
this.enabled = Services.prefs.getBoolPref("signon.rememberSignons");
55
this.storageEnabled = Services.prefs.getBoolPref("signon.storeSignons");
56
this.formlessCaptureEnabled = Services.prefs.getBoolPref(
57
"signon.formlessCapture.enabled"
58
);
59
this.generationAvailable = Services.prefs.getBoolPref(
60
"signon.generation.available"
61
);
62
this.generationEnabled = Services.prefs.getBoolPref(
63
"signon.generation.enabled"
64
);
65
this.insecureAutofill = Services.prefs.getBoolPref(
66
"signon.autofillForms.http"
67
);
68
this.includeOtherSubdomainsInLookup = Services.prefs.getBoolPref(
69
"signon.includeOtherSubdomainsInLookup"
70
);
71
this.managementURI = Services.prefs.getStringPref(
72
"signon.management.overrideURI",
73
null
74
);
75
this.privateBrowsingCaptureEnabled = Services.prefs.getBoolPref(
76
"signon.privateBrowsingCapture.enabled"
77
);
78
this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
79
this.showAutoCompleteFooter = Services.prefs.getBoolPref(
80
"signon.showAutoCompleteFooter"
81
);
82
this.storeWhenAutocompleteOff = Services.prefs.getBoolPref(
83
"signon.storeWhenAutocompleteOff"
84
);
85
this.userInputRequiredToCapture = Services.prefs.getBoolPref(
86
"signon.userInputRequiredToCapture.enabled"
87
);
88
},
89
90
createLogger(aLogPrefix) {
91
let getMaxLogLevel = () => {
92
return this.debug ? "Debug" : "Warn";
93
};
94
95
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
96
let consoleOptions = {
97
maxLogLevel: getMaxLogLevel(),
98
prefix: aLogPrefix,
99
};
100
let logger = console.createInstance(consoleOptions);
101
102
// Watch for pref changes and update this.debug and the maxLogLevel for created loggers
103
Services.prefs.addObserver("signon.debug", () => {
104
this.debug = Services.prefs.getBoolPref("signon.debug");
105
if (logger) {
106
logger.maxLogLevel = getMaxLogLevel();
107
}
108
});
109
110
return logger;
111
},
112
113
/**
114
* Due to the way the signons2.txt file is formatted, we need to make
115
* sure certain field values or characters do not cause the file to
116
* be parsed incorrectly. Reject origins that we can't store correctly.
117
*
118
* @throws String with English message in case validation failed.
119
*/
120
checkOriginValue(aOrigin) {
121
// Nulls are invalid, as they don't round-trip well. Newlines are also
122
// invalid for any field stored as plaintext, and an origin made of a
123
// single dot cannot be stored in the legacy format.
124
if (
125
aOrigin == "." ||
126
aOrigin.includes("\r") ||
127
aOrigin.includes("\n") ||
128
aOrigin.includes("\0")
129
) {
130
throw new Error("Invalid origin");
131
}
132
},
133
134
/**
135
* Due to the way the signons2.txt file is formatted, we need to make
136
* sure certain field values or characters do not cause the file to
137
* be parsed incorrectly. Reject logins that we can't store correctly.
138
*
139
* @throws String with English message in case validation failed.
140
*/
141
checkLoginValues(aLogin) {
142
function badCharacterPresent(l, c) {
143
return (
144
(l.formActionOrigin && l.formActionOrigin.includes(c)) ||
145
(l.httpRealm && l.httpRealm.includes(c)) ||
146
l.origin.includes(c) ||
147
l.usernameField.includes(c) ||
148
l.passwordField.includes(c)
149
);
150
}
151
152
// Nulls are invalid, as they don't round-trip well.
153
// Mostly not a formatting problem, although ".\0" can be quirky.
154
if (badCharacterPresent(aLogin, "\0")) {
155
throw new Error("login values can't contain nulls");
156
}
157
158
// In theory these nulls should just be rolled up into the encrypted
159
// values, but nsISecretDecoderRing doesn't use nsStrings, so the
160
// nulls cause truncation. Check for them here just to avoid
161
// unexpected round-trip surprises.
162
if (aLogin.username.includes("\0") || aLogin.password.includes("\0")) {
163
throw new Error("login values can't contain nulls");
164
}
165
166
// Newlines are invalid for any field stored as plaintext.
167
if (
168
badCharacterPresent(aLogin, "\r") ||
169
badCharacterPresent(aLogin, "\n")
170
) {
171
throw new Error("login values can't contain newlines");
172
}
173
174
// A line with just a "." can have special meaning.
175
if (aLogin.usernameField == "." || aLogin.formActionOrigin == ".") {
176
throw new Error("login values can't be periods");
177
}
178
179
// An origin with "\ \(" won't roundtrip.
180
// eg host="foo (", realm="bar" --> "foo ( (bar)"
181
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
182
if (aLogin.origin.includes(" (")) {
183
throw new Error("bad parens in origin");
184
}
185
},
186
187
/**
188
* Returns a new XPCOM property bag with the provided properties.
189
*
190
* @param {Object} aProperties
191
* Each property of this object is copied to the property bag. This
192
* parameter can be omitted to return an empty property bag.
193
*
194
* @return A new property bag, that is an instance of nsIWritablePropertyBag,
195
* nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
196
*/
197
newPropertyBag(aProperties) {
198
let propertyBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
199
Ci.nsIWritablePropertyBag
200
);
201
if (aProperties) {
202
for (let [name, value] of Object.entries(aProperties)) {
203
propertyBag.setProperty(name, value);
204
}
205
}
206
return propertyBag
207
.QueryInterface(Ci.nsIPropertyBag)
208
.QueryInterface(Ci.nsIPropertyBag2)
209
.QueryInterface(Ci.nsIWritablePropertyBag2);
210
},
211
212
/**
213
* Helper to avoid the property bags when calling
214
* Services.logins.searchLogins from JS.
215
* @deprecated Use Services.logins.searchLoginsAsync instead.
216
*
217
* @param {Object} aSearchOptions - A regular JS object to copy to a property bag before searching
218
* @return {nsILoginInfo[]} - The result of calling searchLogins.
219
*/
220
searchLoginsWithObject(aSearchOptions) {
221
return Services.logins.searchLogins(this.newPropertyBag(aSearchOptions));
222
},
223
224
/**
225
* @param {string} aURL
226
* @returns {string} which is the hostPort of aURL if supported by the scheme
227
* otherwise, returns the original aURL.
228
*/
229
maybeGetHostPortForURL(aURL) {
230
try {
231
let uri = Services.io.newURI(aURL);
232
return uri.hostPort;
233
} catch (ex) {
234
// No need to warn for javascript:/data:/about:/chrome:/etc.
235
}
236
return aURL;
237
},
238
239
/**
240
* Get the parts of the URL we want for identification.
241
* Strip out things like the userPass portion and handle javascript:.
242
*/
243
getLoginOrigin(uriString, allowJS) {
244
let realm = "";
245
try {
246
let uri = Services.io.newURI(uriString);
247
248
if (allowJS && uri.scheme == "javascript") {
249
return "javascript:";
250
}
251
// TODO: Bug 1559205 - Add support for moz-proxy
252
253
// Build this manually instead of using prePath to avoid including the userPass portion.
254
realm = uri.scheme + "://" + uri.displayHostPort;
255
} catch (e) {
256
// bug 159484 - disallow url types that don't support a hostPort.
257
// (although we handle "javascript:..." as a special case above.)
258
log.warn("Couldn't parse origin for", uriString, e);
259
realm = null;
260
}
261
262
return realm;
263
},
264
265
getFormActionOrigin(form) {
266
let uriString = form.action;
267
268
// A blank or missing action submits to where it came from.
269
if (uriString == "") {
270
// ala bug 297761
271
uriString = form.baseURI;
272
}
273
274
return this.getLoginOrigin(uriString, true);
275
},
276
277
/**
278
* @param {String} aLoginOrigin - An origin value from a stored login's
279
* origin or formActionOrigin properties.
280
* @param {String} aSearchOrigin - The origin that was are looking to match
281
* with aLoginOrigin. This would normally come
282
* from a form or page that we are considering.
283
* @param {nsILoginFindOptions} aOptions - Options to affect whether the origin
284
* from the login (aLoginOrigin) is a
285
* match for the origin we're looking
286
* for (aSearchOrigin).
287
*/
288
isOriginMatching(
289
aLoginOrigin,
290
aSearchOrigin,
291
aOptions = {
292
schemeUpgrades: false,
293
acceptWildcardMatch: false,
294
acceptDifferentSubdomains: false,
295
}
296
) {
297
if (aLoginOrigin == aSearchOrigin) {
298
return true;
299
}
300
301
if (!aOptions) {
302
return false;
303
}
304
305
if (aOptions.acceptWildcardMatch && aLoginOrigin == "") {
306
return true;
307
}
308
309
try {
310
let loginURI = Services.io.newURI(aLoginOrigin);
311
let searchURI = Services.io.newURI(aSearchOrigin);
312
let schemeMatches =
313
loginURI.scheme == "http" && searchURI.scheme == "https";
314
315
if (aOptions.acceptDifferentSubdomains) {
316
let loginBaseDomain = Services.eTLD.getBaseDomain(loginURI);
317
let searchBaseDomain = Services.eTLD.getBaseDomain(searchURI);
318
if (
319
loginBaseDomain == searchBaseDomain &&
320
(loginURI.scheme == searchURI.scheme ||
321
(aOptions.schemeUpgrades && schemeMatches))
322
) {
323
return true;
324
}
325
}
326
327
if (
328
aOptions.schemeUpgrades &&
329
loginURI.host == searchURI.host &&
330
schemeMatches &&
331
loginURI.port == searchURI.port
332
) {
333
return true;
334
}
335
} catch (ex) {
336
// newURI will throw for some values e.g. chrome://FirefoxAccounts
337
// uri.host and uri.port will throw for some values e.g. javascript:
338
return false;
339
}
340
341
return false;
342
},
343
344
doLoginsMatch(
345
aLogin1,
346
aLogin2,
347
{ ignorePassword = false, ignoreSchemes = false }
348
) {
349
if (
350
aLogin1.httpRealm != aLogin2.httpRealm ||
351
aLogin1.username != aLogin2.username
352
) {
353
return false;
354
}
355
356
if (!ignorePassword && aLogin1.password != aLogin2.password) {
357
return false;
358
}
359
360
if (ignoreSchemes) {
361
let login1HostPort = this.maybeGetHostPortForURL(aLogin1.origin);
362
let login2HostPort = this.maybeGetHostPortForURL(aLogin2.origin);
363
if (login1HostPort != login2HostPort) {
364
return false;
365
}
366
367
if (
368
aLogin1.formActionOrigin != "" &&
369
aLogin2.formActionOrigin != "" &&
370
this.maybeGetHostPortForURL(aLogin1.formActionOrigin) !=
371
this.maybeGetHostPortForURL(aLogin2.formActionOrigin)
372
) {
373
return false;
374
}
375
} else {
376
if (aLogin1.origin != aLogin2.origin) {
377
return false;
378
}
379
380
// If either formActionOrigin is blank (but not null), then match.
381
if (
382
aLogin1.formActionOrigin != "" &&
383
aLogin2.formActionOrigin != "" &&
384
aLogin1.formActionOrigin != aLogin2.formActionOrigin
385
) {
386
return false;
387
}
388
}
389
390
// The .usernameField and .passwordField values are ignored.
391
392
return true;
393
},
394
395
/**
396
* Creates a new login object that results by modifying the given object with
397
* the provided data.
398
*
399
* @param aOldStoredLogin
400
* Existing nsILoginInfo object to modify.
401
* @param aNewLoginData
402
* The new login values, either as nsILoginInfo or nsIProperyBag.
403
*
404
* @return The newly created nsILoginInfo object.
405
*
406
* @throws String with English message in case validation failed.
407
*/
408
buildModifiedLogin(aOldStoredLogin, aNewLoginData) {
409
function bagHasProperty(aPropName) {
410
try {
411
aNewLoginData.getProperty(aPropName);
412
return true;
413
} catch (ex) {}
414
return false;
415
}
416
417
aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
418
419
let newLogin;
420
if (aNewLoginData instanceof Ci.nsILoginInfo) {
421
// Clone the existing login to get its nsILoginMetaInfo, then init it
422
// with the replacement nsILoginInfo data from the new login.
423
newLogin = aOldStoredLogin.clone();
424
newLogin.init(
425
aNewLoginData.origin,
426
aNewLoginData.formActionOrigin,
427
aNewLoginData.httpRealm,
428
aNewLoginData.username,
429
aNewLoginData.password,
430
aNewLoginData.usernameField,
431
aNewLoginData.passwordField
432
);
433
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
434
435
// Automatically update metainfo when password is changed.
436
if (newLogin.password != aOldStoredLogin.password) {
437
newLogin.timePasswordChanged = Date.now();
438
}
439
} else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
440
// Clone the existing login, along with all its properties.
441
newLogin = aOldStoredLogin.clone();
442
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
443
444
// Automatically update metainfo when password is changed.
445
// (Done before the main property updates, lest the caller be
446
// explicitly updating both .password and .timePasswordChanged)
447
if (bagHasProperty("password")) {
448
let newPassword = aNewLoginData.getProperty("password");
449
if (newPassword != aOldStoredLogin.password) {
450
newLogin.timePasswordChanged = Date.now();
451
}
452
}
453
454
for (let prop of aNewLoginData.enumerator) {
455
switch (prop.name) {
456
// nsILoginInfo (fall through)
457
case "origin":
458
case "httpRealm":
459
case "formActionOrigin":
460
case "username":
461
case "password":
462
case "usernameField":
463
case "passwordField":
464
// nsILoginMetaInfo (fall through)
465
case "guid":
466
case "timeCreated":
467
case "timeLastUsed":
468
case "timePasswordChanged":
469
case "timesUsed":
470
newLogin[prop.name] = prop.value;
471
break;
472
473
// Fake property, allows easy incrementing.
474
case "timesUsedIncrement":
475
newLogin.timesUsed += prop.value;
476
break;
477
478
// Fail if caller requests setting an unknown property.
479
default:
480
throw new Error("Unexpected propertybag item: " + prop.name);
481
}
482
}
483
} else {
484
throw new Error("newLoginData needs an expected interface!");
485
}
486
487
// Sanity check the login
488
if (newLogin.origin == null || !newLogin.origin.length) {
489
throw new Error("Can't add a login with a null or empty origin.");
490
}
491
492
// For logins w/o a username, set to "", not null.
493
if (newLogin.username == null) {
494
throw new Error("Can't add a login with a null username.");
495
}
496
497
if (newLogin.password == null || !newLogin.password.length) {
498
throw new Error("Can't add a login with a null or empty password.");
499
}
500
501
if (newLogin.formActionOrigin || newLogin.formActionOrigin == "") {
502
// We have a form submit URL. Can't have a HTTP realm.
503
if (newLogin.httpRealm != null) {
504
throw new Error(
505
"Can't add a login with both a httpRealm and formActionOrigin."
506
);
507
}
508
} else if (newLogin.httpRealm) {
509
// We have a HTTP realm. Can't have a form submit URL.
510
if (newLogin.formActionOrigin != null) {
511
throw new Error(
512
"Can't add a login with both a httpRealm and formActionOrigin."
513
);
514
}
515
} else {
516
// Need one or the other!
517
throw new Error(
518
"Can't add a login without a httpRealm or formActionOrigin."
519
);
520
}
521
522
// Throws if there are bogus values.
523
this.checkLoginValues(newLogin);
524
525
return newLogin;
526
},
527
528
/**
529
* Remove http: logins when there is an https: login with the same username and hostPort.
530
* Sort order is preserved.
531
*
532
* @param {nsILoginInfo[]} logins
533
* A list of logins we want to process for shadowing.
534
* @returns {nsILoginInfo[]} A subset of of the passed logins.
535
*/
536
shadowHTTPLogins(logins) {
537
/**
538
* Map a (hostPort, username) to a boolean indicating whether `logins`
539
* contains an https: login for that combo.
540
*/
541
let hasHTTPSByHostPortUsername = new Map();
542
for (let login of logins) {
543
let key = this.getUniqueKeyForLogin(login, ["hostPort", "username"]);
544
let hasHTTPSlogin = hasHTTPSByHostPortUsername.get(key) || false;
545
let loginURI = Services.io.newURI(login.origin);
546
hasHTTPSByHostPortUsername.set(
547
key,
548
loginURI.scheme == "https" || hasHTTPSlogin
549
);
550
}
551
552
return logins.filter(login => {
553
let key = this.getUniqueKeyForLogin(login, ["hostPort", "username"]);
554
let loginURI = Services.io.newURI(login.origin);
555
if (loginURI.scheme == "http" && hasHTTPSByHostPortUsername.get(key)) {
556
// If this is an http: login and we have an https: login for the
557
// (hostPort, username) combo then remove it.
558
return false;
559
}
560
return true;
561
});
562
},
563
564
/**
565
* Generate a unique key string from a login.
566
* @param {nsILoginInfo} login
567
* @param {string[]} uniqueKeys containing nsILoginInfo attribute names or "hostPort"
568
* @returns {string} to use as a key in a Map
569
*/
570
getUniqueKeyForLogin(login, uniqueKeys) {
571
const KEY_DELIMITER = ":";
572
return uniqueKeys.reduce((prev, key) => {
573
let val = null;
574
if (key == "hostPort") {
575
val = Services.io.newURI(login.origin).hostPort;
576
} else {
577
val = login[key];
578
}
579
580
return prev + KEY_DELIMITER + val;
581
}, "");
582
},
583
584
/**
585
* Removes duplicates from a list of logins while preserving the sort order.
586
*
587
* @param {nsILoginInfo[]} logins
588
* A list of logins we want to deduplicate.
589
* @param {string[]} [uniqueKeys = ["username", "password"]]
590
* A list of login attributes to use as unique keys for the deduplication.
591
* @param {string[]} [resolveBy = ["timeLastUsed"]]
592
* Ordered array of keyword strings used to decide which of the
593
* duplicates should be used. "scheme" would prefer the login that has
594
* a scheme matching `preferredOrigin`'s if there are two logins with
595
* the same `uniqueKeys`. The default preference to distinguish two
596
* logins is `timeLastUsed`. If there is no preference between two
597
* logins, the first one found wins.
598
* @param {string} [preferredOrigin = undefined]
599
* String representing the origin to use for preferring one login over
600
* another when they are dupes. This is used with "scheme" for
601
* `resolveBy` so the scheme from this origin will be preferred.
602
* @param {string} [preferredFormActionOrigin = undefined]
603
* String representing the action origin to use for preferring one login over
604
* another when they are dupes. This is used with "actionOrigin" for
605
* `resolveBy` so the scheme from this action origin will be preferred.
606
*
607
* @returns {nsILoginInfo[]} list of unique logins.
608
*/
609
dedupeLogins(
610
logins,
611
uniqueKeys = ["username", "password"],
612
resolveBy = ["timeLastUsed"],
613
preferredOrigin = undefined,
614
preferredFormActionOrigin = undefined
615
) {
616
if (!preferredOrigin) {
617
if (resolveBy.includes("scheme")) {
618
throw new Error(
619
"dedupeLogins: `preferredOrigin` is required in order to " +
620
"prefer schemes which match it."
621
);
622
}
623
if (resolveBy.includes("subdomain")) {
624
throw new Error(
625
"dedupeLogins: `preferredOrigin` is required in order to " +
626
"prefer subdomains which match it."
627
);
628
}
629
}
630
631
let preferredOriginScheme;
632
if (preferredOrigin) {
633
try {
634
preferredOriginScheme = Services.io.newURI(preferredOrigin).scheme;
635
} catch (ex) {
636
// Handle strings that aren't valid URIs e.g. chrome://FirefoxAccounts
637
}
638
}
639
640
if (!preferredOriginScheme && resolveBy.includes("scheme")) {
641
log.warn(
642
"dedupeLogins: Deduping with a scheme preference but couldn't " +
643
"get the preferred origin scheme."
644
);
645
}
646
647
// We use a Map to easily lookup logins by their unique keys.
648
let loginsByKeys = new Map();
649
650
/**
651
* @return {bool} whether `login` is preferred over its duplicate (considering `uniqueKeys`)
652
* `existingLogin`.
653
*
654
* `resolveBy` is a sorted array so we can return true the first time `login` is preferred
655
* over the existingLogin.
656
*/
657
function isLoginPreferred(existingLogin, login) {
658
if (!resolveBy || !resolveBy.length) {
659
// If there is no preference, prefer the existing login.
660
return false;
661
}
662
663
for (let preference of resolveBy) {
664
switch (preference) {
665
case "actionOrigin": {
666
if (!preferredFormActionOrigin) {
667
break;
668
}
669
if (
670
LoginHelper.isOriginMatching(
671
existingLogin.formActionOrigin,
672
preferredFormActionOrigin,
673
{ schemeUpgrades: LoginHelper.schemeUpgrades }
674
) &&
675
!LoginHelper.isOriginMatching(
676
login.formActionOrigin,
677
preferredFormActionOrigin,
678
{ schemeUpgrades: LoginHelper.schemeUpgrades }
679
)
680
) {
681
return false;
682
}
683
break;
684
}
685
case "scheme": {
686
if (!preferredOriginScheme) {
687
break;
688
}
689
690
try {
691
// Only `origin` is currently considered
692
let existingLoginURI = Services.io.newURI(existingLogin.origin);
693
let loginURI = Services.io.newURI(login.origin);
694
// If the schemes of the two logins are the same or neither match the
695
// preferredOriginScheme then we have no preference and look at the next resolveBy.
696
if (
697
loginURI.scheme == existingLoginURI.scheme ||
698
(loginURI.scheme != preferredOriginScheme &&
699
existingLoginURI.scheme != preferredOriginScheme)
700
) {
701
break;
702
}
703
704
return loginURI.scheme == preferredOriginScheme;
705
} catch (ex) {
706
// Some URLs aren't valid nsIURI (e.g. chrome://FirefoxAccounts)
707
log.debug(
708
"dedupeLogins/shouldReplaceExisting: Error comparing schemes:",
709
existingLogin.origin,
710
login.origin,
711
"preferredOrigin:",
712
preferredOrigin,
713
ex
714
);
715
}
716
break;
717
}
718
case "subdomain": {
719
// Replace the existing login only if the new login is an exact match on the host.
720
let existingLoginURI = Services.io.newURI(existingLogin.origin);
721
let newLoginURI = Services.io.newURI(login.origin);
722
let preferredOriginURI = Services.io.newURI(preferredOrigin);
723
if (
724
existingLoginURI.hostPort != preferredOriginURI.hostPort &&
725
newLoginURI.hostPort == preferredOriginURI.hostPort
726
) {
727
return true;
728
}
729
if (
730
existingLoginURI.host != preferredOriginURI.host &&
731
newLoginURI.host == preferredOriginURI.host
732
) {
733
return true;
734
}
735
// if the existing login host *is* a match and the new one isn't
736
// we explicitly want to keep the existing one
737
if (
738
existingLoginURI.host == preferredOriginURI.host &&
739
newLoginURI.host != preferredOriginURI.host
740
) {
741
return false;
742
}
743
break;
744
}
745
case "timeLastUsed":
746
case "timePasswordChanged": {
747
// If we find a more recent login for the same key, replace the existing one.
748
let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo)[
749
preference
750
];
751
let storedLoginDate = existingLogin.QueryInterface(
752
Ci.nsILoginMetaInfo
753
)[preference];
754
if (loginDate == storedLoginDate) {
755
break;
756
}
757
758
return loginDate > storedLoginDate;
759
}
760
default: {
761
throw new Error(
762
"dedupeLogins: Invalid resolveBy preference: " + preference
763
);
764
}
765
}
766
}
767
768
return false;
769
}
770
771
for (let login of logins) {
772
let key = this.getUniqueKeyForLogin(login, uniqueKeys);
773
774
if (loginsByKeys.has(key)) {
775
if (!isLoginPreferred(loginsByKeys.get(key), login)) {
776
// If there is no preference for the new login, use the existing one.
777
continue;
778
}
779
}
780
loginsByKeys.set(key, login);
781
}
782
783
// Return the map values in the form of an array.
784
return [...loginsByKeys.values()];
785
},
786
787
/**
788
* Open the password manager window.
789
*
790
* @param {Window} window
791
* the window from where we want to open the dialog
792
*
793
* @param {object?} args
794
* params for opening the password manager
795
* @param {string} [args.filterString=""]
796
* the domain (not origin) to pass to the login manager dialog
797
* to pre-filter the results
798
* @param {string} args.entryPoint
799
* The name of the entry point, used for telemetry
800
*/
801
openPasswordManager(window, { filterString = "", entryPoint = "" } = {}) {
802
if (this.managementURI && window.openTrustedLinkIn) {
803
let managementURL = this.managementURI.replace(
804
"%DOMAIN%",
805
window.encodeURIComponent(filterString)
806
);
807
// We assume that managementURL has a '?' already
808
window.openTrustedLinkIn(
809
managementURL + `&entryPoint=${entryPoint}`,
810
"tab"
811
);
812
return;
813
}
814
Services.telemetry.recordEvent("pwmgr", "open_management", entryPoint);
815
let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
816
if (win) {
817
win.setFilter(filterString);
818
win.focus();
819
} else {
820
window.openDialog(
822
"Toolkit:PasswordManager",
823
"",
824
{ filterString }
825
);
826
}
827
},
828
829
/**
830
* Checks if a field type is username compatible.
831
*
832
* @param {Element} element
833
* the field we want to check.
834
*
835
* @returns {Boolean} true if the field type is one
836
* of the username types.
837
*/
838
isUsernameFieldType(element) {
839
if (ChromeUtils.getClassName(element) !== "HTMLInputElement") {
840
return false;
841
}
842
843
if (!element.isConnected) {
844
// If the element isn't connected then it isn't visible to the user so
845
// shouldn't be considered. It must have been connected in the past.
846
return false;
847
}
848
849
let fieldType = element.hasAttribute("type")
850
? element.getAttribute("type").toLowerCase()
851
: element.type;
852
if (
853
!(
854
fieldType == "text" ||
855
fieldType == "email" ||
856
fieldType == "url" ||
857
fieldType == "tel" ||
858
fieldType == "number"
859
)
860
) {
861
return false;
862
}
863
864
let acFieldName = element.getAutocompleteInfo().fieldName;
865
if (
866
!(
867
acFieldName == "username" ||
868
// Bug 1540154: Some sites use tel/email on their username fields.
869
acFieldName == "email" ||
870
acFieldName == "tel" ||
871
acFieldName == "tel-national" ||
872
acFieldName == "off" ||
873
acFieldName == "on" ||
874
acFieldName == ""
875
)
876
) {
877
return false;
878
}
879
return true;
880
},
881
882
/**
883
* For each login, add the login to the password manager if a similar one
884
* doesn't already exist. Merge it otherwise with the similar existing ones.
885
*
886
* @param {Object[]} loginDatas - For each login, the data that needs to be added.
887
* @returns {nsILoginInfo[]} the newly added logins, filtered if no login was added.
888
*/
889
async maybeImportLogins(loginDatas) {
890
let loginsToAdd = [];
891
let loginMap = new Map();
892
for (let loginData of loginDatas) {
893
// create a new login
894
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
895
Ci.nsILoginInfo
896
);
897
login.init(
898
loginData.origin,
899
loginData.formActionOrigin ||
900
(typeof loginData.httpRealm == "string" ? null : ""),
901
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null,
902
loginData.username,
903
loginData.password,
904
loginData.usernameElement || "",
905
loginData.passwordElement || ""
906
);
907
908
login.QueryInterface(Ci.nsILoginMetaInfo);
909
login.timeCreated = loginData.timeCreated;
910
login.timeLastUsed = loginData.timeLastUsed || loginData.timeCreated;
911
login.timePasswordChanged =
912
loginData.timePasswordChanged || loginData.timeCreated;
913
login.timesUsed = loginData.timesUsed || 1;
914
915
try {
916
// Ensure we only send checked logins through, since the validation is optimized
917
// out from the bulk APIs below us.
918
this.checkLoginValues(login);
919
} catch (e) {
920
Cu.reportError(e);
921
continue;
922
}
923
924
// First, we need to check the logins that we've already decided to add, to
925
// see if this is a duplicate. This should mirror the logic below for
926
// existingLogins, but only for the array of logins we're adding.
927
let newLogins = loginMap.get(login.origin) || [];
928
if (!newLogins) {
929
loginMap.set(login.origin, newLogins);
930
} else {
931
if (newLogins.some(l => login.matches(l, false /* ignorePassword */))) {
932
continue;
933
}
934
let foundMatchingNewLogin = false;
935
for (let newLogin of newLogins) {
936
if (login.username == newLogin.username) {
937
foundMatchingNewLogin = true;
938
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
939
if (
940
(login.password != newLogin.password) &
941
(login.timePasswordChanged > newLogin.timePasswordChanged)
942
) {
943
// if a login with the same username and different password already exists and it's older
944
// than the current one, update its password and timestamp.
945
newLogin.password = login.password;
946
newLogin.timePasswordChanged = login.timePasswordChanged;
947
}
948
}
949
}
950
951
if (foundMatchingNewLogin) {
952
continue;
953
}
954
}
955
956
// While here we're passing formActionOrigin and httpRealm, they could be empty/null and get
957
// ignored in that case, leading to multiple logins for the same username.
958
let existingLogins = Services.logins.findLogins(
959
login.origin,
960
login.formActionOrigin,
961
login.httpRealm
962
);
963
// Check for an existing login that matches *including* the password.
964
// If such a login exists, we do not need to add a new login.
965
if (
966
existingLogins.some(l => login.matches(l, false /* ignorePassword */))
967
) {
968
continue;
969
}
970
// Now check for a login with the same username, where it may be that we have an
971
// updated password.
972
let foundMatchingLogin = false;
973
for (let existingLogin of existingLogins) {
974
if (login.username == existingLogin.username) {
975
foundMatchingLogin = true;
976
existingLogin.QueryInterface(Ci.nsILoginMetaInfo);
977
if (
978
(login.password != existingLogin.password) &
979
(login.timePasswordChanged > existingLogin.timePasswordChanged)
980
) {
981
// if a login with the same username and different password already exists and it's older
982
// than the current one, update its password and timestamp.
983
let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
984
Ci.nsIWritablePropertyBag
985
);
986
propBag.setProperty("password", login.password);
987
propBag.setProperty(
988
"timePasswordChanged",
989
login.timePasswordChanged
990
);
991
Services.logins.modifyLogin(existingLogin, propBag);
992
}
993
}
994
}
995
// if the new login is an update or is older than an exiting login, don't add it.
996
if (foundMatchingLogin) {
997
continue;
998
}
999
1000
newLogins.push(login);
1001
loginsToAdd.push(login);
1002
}
1003
if (!loginsToAdd.length) {
1004
return [];
1005
}
1006
return Services.logins.addLogins(loginsToAdd);
1007
},
1008
1009
/**
1010
* Convert an array of nsILoginInfo to vanilla JS objects suitable for
1011
* sending over IPC.
1012
*
1013
* NB: All members of nsILoginInfo and nsILoginMetaInfo are strings.
1014
*/
1015
loginsToVanillaObjects(logins) {
1016
return logins.map(this.loginToVanillaObject);
1017
},
1018
1019
/**
1020
* Same as above, but for a single login.
1021
*/
1022
loginToVanillaObject(login) {
1023
let obj = {};
1024
for (let i in login.QueryInterface(Ci.nsILoginMetaInfo)) {
1025
if (typeof login[i] !== "function") {
1026
obj[i] = login[i];
1027
}
1028
}
1029
1030
return obj;
1031
},
1032
1033
/**
1034
* Convert an object received from IPC into an nsILoginInfo (with guid).
1035
*/
1036
vanillaObjectToLogin(login) {
1037
let formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
1038
Ci.nsILoginInfo
1039
);
1040
formLogin.init(
1041
login.origin,
1042
login.formActionOrigin,
1043
login.httpRealm,
1044
login.username,
1045
login.password,
1046
login.usernameField,
1047
login.passwordField
1048
);
1049
1050
formLogin.QueryInterface(Ci.nsILoginMetaInfo);
1051
for (let prop of [
1052
"guid",
1053
"timeCreated",
1054
"timeLastUsed",
1055
"timePasswordChanged",
1056
"timesUsed",
1057
]) {
1058
formLogin[prop] = login[prop];
1059
}
1060
return formLogin;
1061
},
1062
1063
/**
1064
* As above, but for an array of objects.
1065
*/
1066
vanillaObjectsToLogins(logins) {
1067
return logins.map(this.vanillaObjectToLogin);
1068
},
1069
1070
/**
1071
* Returns true if the user has a master password set and false otherwise.
1072
*/
1073
isMasterPasswordSet() {
1074
let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
1075
Ci.nsIPK11TokenDB
1076
);
1077
let token = tokenDB.getInternalKeyToken();
1078
return token.hasPassword;
1079
},
1080
1081
/**
1082
* Send a notification when stored data is changed.
1083
*/
1084
notifyStorageChanged(changeType, data) {
1085
let dataObject = data;
1086
// Can't pass a raw JS string or array though notifyObservers(). :-(
1087
if (Array.isArray(data)) {
1088
dataObject = Cc["@mozilla.org/array;1"].createInstance(
1089
Ci.nsIMutableArray
1090
);
1091
for (let i = 0; i < data.length; i++) {
1092
dataObject.appendElement(data[i]);
1093
}
1094
} else if (typeof data == "string") {
1095
dataObject = Cc["@mozilla.org/supports-string;1"].createInstance(
1096
Ci.nsISupportsString
1097
);
1098
dataObject.data = data;
1099
}
1100
Services.obs.notifyObservers(
1101
dataObject,
1102
"passwordmgr-storage-changed",
1103
changeType
1104
);
1105
},
1106
1107
isUserFacingLogin(login) {
1108
return login.origin != "chrome://FirefoxAccounts"; // FXA_PWDMGR_HOST
1109
},
1110
1111
async getAllUserFacingLogins() {
1112
try {
1113
let logins = await Services.logins.getAllLoginsAsync();
1114
return logins.filter(this.isUserFacingLogin);
1115
} catch (e) {
1116
if (e.result == Cr.NS_ERROR_ABORT) {
1117
// If the user cancels the MP prompt then return no logins.
1118
return [];
1119
}
1120
throw e;
1121
}
1122
},
1123
1124
createLoginAlreadyExistsError(guid) {
1125
// The GUID is stored in an nsISupportsString here because we cannot pass
1126
// raw JS objects within Components.Exception due to bug 743121.
1127
let guidSupportsString = Cc[
1128
"@mozilla.org/supports-string;1"
1129
].createInstance(Ci.nsISupportsString);
1130
guidSupportsString.data = guid;
1131
return Components.Exception("This login already exists.", {
1132
data: guidSupportsString,
1133
});
1134
},
1135
};
1136
1137
LoginHelper.init();
1138
1139
XPCOMUtils.defineLazyPreferenceGetter(
1140
LoginHelper,
1141
"showInsecureFieldWarning",
1142
"security.insecure_field_warning.contextual.enabled"
1143
);
1144
1145
XPCOMUtils.defineLazyGetter(this, "log", () => {
1146
return LoginHelper.createLogger("LoginHelper");
1147
});