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
* nsIAutoCompleteResult and nsILoginAutoCompleteSearch implementations for saved logins.
7
*/
8
9
"use strict";
10
11
const EXPORTED_SYMBOLS = ["LoginAutoComplete", "LoginAutoCompleteResult"];
12
13
const { XPCOMUtils } = ChromeUtils.import(
15
);
16
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
17
18
ChromeUtils.defineModuleGetter(
19
this,
20
"BrowserUtils",
22
);
23
ChromeUtils.defineModuleGetter(
24
this,
25
"InsecurePasswordUtils",
27
);
28
ChromeUtils.defineModuleGetter(
29
this,
30
"LoginFormFactory",
32
);
33
ChromeUtils.defineModuleGetter(
34
this,
35
"LoginHelper",
37
);
38
ChromeUtils.defineModuleGetter(
39
this,
40
"LoginManagerChild",
42
);
43
44
XPCOMUtils.defineLazyServiceGetter(
45
this,
46
"formFillController",
47
"@mozilla.org/satchel/form-fill-controller;1",
48
Ci.nsIFormFillController
49
);
50
XPCOMUtils.defineLazyPreferenceGetter(
51
this,
52
"SHOULD_SHOW_ORIGIN",
53
"signon.showAutoCompleteOrigins"
54
);
55
56
XPCOMUtils.defineLazyGetter(this, "log", () => {
57
return LoginHelper.createLogger("LoginAutoCompleteResult");
58
});
59
XPCOMUtils.defineLazyGetter(this, "passwordMgrBundle", () => {
60
return Services.strings.createBundle(
62
);
63
});
64
65
function loginSort(formHostPort, a, b) {
66
let maybeHostPortA = LoginHelper.maybeGetHostPortForURL(a.origin);
67
let maybeHostPortB = LoginHelper.maybeGetHostPortForURL(b.origin);
68
if (formHostPort == maybeHostPortA && formHostPort != maybeHostPortB) {
69
return -1;
70
}
71
if (formHostPort != maybeHostPortA && formHostPort == maybeHostPortB) {
72
return 1;
73
}
74
75
if (a.httpRealm !== b.httpRealm) {
76
// Sort HTTP auth. logins after form logins for the same origin.
77
if (b.httpRealm === null) {
78
return 1;
79
}
80
if (a.httpRealm === null) {
81
return -1;
82
}
83
}
84
85
let userA = a.username.toLowerCase();
86
let userB = b.username.toLowerCase();
87
88
if (userA < userB) {
89
return -1;
90
}
91
92
if (userA > userB) {
93
return 1;
94
}
95
96
return 0;
97
}
98
99
function findDuplicates(loginList) {
100
let seen = new Set();
101
let duplicates = new Set();
102
for (let login of loginList) {
103
if (seen.has(login.username)) {
104
duplicates.add(login.username);
105
}
106
seen.add(login.username);
107
}
108
return duplicates;
109
}
110
111
function getLocalizedString(key, formatArgs = null) {
112
if (formatArgs) {
113
return passwordMgrBundle.formatStringFromName(key, formatArgs);
114
}
115
return passwordMgrBundle.GetStringFromName(key);
116
}
117
118
class AutocompleteItem {
119
constructor(style) {
120
this.comment = "";
121
this.style = style;
122
this.value = "";
123
}
124
125
removeFromStorage() {
126
/* Do nothing by default */
127
}
128
}
129
130
class InsecureLoginFormAutocompleteItem extends AutocompleteItem {
131
constructor() {
132
super("insecureWarning");
133
134
XPCOMUtils.defineLazyGetter(this, "label", () => {
135
let learnMoreString = getLocalizedString("insecureFieldWarningLearnMore");
136
return getLocalizedString("insecureFieldWarningDescription2", [
137
learnMoreString,
138
]);
139
});
140
}
141
}
142
143
class LoginAutocompleteItem extends AutocompleteItem {
144
constructor(
145
login,
146
isPasswordField,
147
dateAndTimeFormatter,
148
duplicateUsernames,
149
actor,
150
isOriginMatched
151
) {
152
super(SHOULD_SHOW_ORIGIN ? "loginWithOrigin" : "login");
153
this._login = login.QueryInterface(Ci.nsILoginMetaInfo);
154
this._actor = actor;
155
156
XPCOMUtils.defineLazyGetter(this, "label", () => {
157
let username = login.username;
158
// If login is empty or duplicated we want to append a modification date to it.
159
if (!username || duplicateUsernames.has(username)) {
160
if (!username) {
161
username = getLocalizedString("noUsername");
162
}
163
let time = dateAndTimeFormatter.format(
164
new Date(login.timePasswordChanged)
165
);
166
username = getLocalizedString("loginHostAge", [username, time]);
167
}
168
return username;
169
});
170
171
XPCOMUtils.defineLazyGetter(this, "value", () => {
172
return isPasswordField ? login.password : login.username;
173
});
174
175
XPCOMUtils.defineLazyGetter(this, "comment", () => {
176
return JSON.stringify({
177
guid: login.guid,
178
comment:
179
isOriginMatched && login.httpRealm === null
180
? getLocalizedString("displaySameOrigin")
181
: login.displayOrigin,
182
});
183
});
184
}
185
186
removeFromStorage() {
187
if (this._actor) {
188
let vanilla = LoginHelper.loginToVanillaObject(this._login);
189
this._actor.sendAsyncMessage("PasswordManager:removeLogin", {
190
login: vanilla,
191
});
192
} else {
193
Services.logins.removeLogin(this._login);
194
}
195
}
196
}
197
198
class GeneratedPasswordAutocompleteItem extends AutocompleteItem {
199
constructor(generatedPassword, willAutoSaveGeneratedPassword) {
200
super("generatedPassword");
201
XPCOMUtils.defineLazyGetter(this, "comment", () => {
202
return JSON.stringify({
203
generatedPassword,
204
willAutoSaveGeneratedPassword,
205
});
206
});
207
this.value = generatedPassword;
208
209
XPCOMUtils.defineLazyGetter(this, "label", () => {
210
return getLocalizedString("useASecurelyGeneratedPassword");
211
});
212
}
213
}
214
215
class LoginsFooterAutocompleteItem extends AutocompleteItem {
216
constructor(hostname) {
217
super("loginsFooter");
218
this.comment = hostname;
219
220
XPCOMUtils.defineLazyGetter(this, "label", () => {
221
return getLocalizedString("viewSavedLogins.label");
222
});
223
}
224
}
225
226
// nsIAutoCompleteResult implementation
227
function LoginAutoCompleteResult(
228
aSearchString,
229
matchingLogins,
230
formOrigin,
231
{
232
generatedPassword,
233
willAutoSaveGeneratedPassword,
234
isSecure,
235
actor,
236
isPasswordField,
237
hostname,
238
}
239
) {
240
let hidingFooterOnPWFieldAutoOpened = false;
241
function isFooterEnabled() {
242
// We need to check LoginHelper.enabled here since the insecure warning should
243
// appear even if pwmgr is disabled but the footer should never appear in that case.
244
if (!LoginHelper.showAutoCompleteFooter || !LoginHelper.enabled) {
245
return false;
246
}
247
248
// Don't show the footer on non-empty password fields as it's not providing
249
// value and only adding noise since a password was already filled.
250
if (isPasswordField && aSearchString && !generatedPassword) {
251
log.debug("Hiding footer: non-empty password field");
252
return false;
253
}
254
255
if (
256
!matchingLogins.length &&
257
!generatedPassword &&
258
isPasswordField &&
259
formFillController.passwordPopupAutomaticallyOpened
260
) {
261
hidingFooterOnPWFieldAutoOpened = true;
262
log.debug(
263
"Hiding footer: no logins and the popup was opened upon focus of the pw. field"
264
);
265
return false;
266
}
267
268
return true;
269
}
270
271
this.searchString = aSearchString;
272
273
// Build up the array of autocomplete rows to display.
274
this._rows = [];
275
276
// Insecure field warning comes first if it applies and is enabled.
277
if (!isSecure && LoginHelper.showInsecureFieldWarning) {
278
this._rows.push(new InsecureLoginFormAutocompleteItem());
279
}
280
281
// Saved login items
282
let formHostPort = LoginHelper.maybeGetHostPortForURL(formOrigin);
283
let logins = matchingLogins.sort(loginSort.bind(null, formHostPort));
284
let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
285
dateStyle: "medium",
286
});
287
let duplicateUsernames = findDuplicates(matchingLogins);
288
289
for (let login of logins) {
290
let item = new LoginAutocompleteItem(
291
login,
292
isPasswordField,
293
dateAndTimeFormatter,
294
duplicateUsernames,
295
actor,
296
LoginHelper.isOriginMatching(login.origin, formOrigin, {
297
schemeUpgrades: LoginHelper.schemeUpgrades,
298
})
299
);
300
this._rows.push(item);
301
}
302
303
// The footer comes last if it's enabled
304
if (isFooterEnabled()) {
305
if (generatedPassword) {
306
this._rows.push(
307
new GeneratedPasswordAutocompleteItem(
308
generatedPassword,
309
willAutoSaveGeneratedPassword
310
)
311
);
312
}
313
this._rows.push(new LoginsFooterAutocompleteItem(hostname));
314
}
315
316
// Determine the result code and default index.
317
if (this.matchCount > 0) {
318
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
319
this.defaultIndex = 0;
320
} else if (hidingFooterOnPWFieldAutoOpened) {
321
// We use a failure result so that the empty results aren't re-used for when
322
// the user tries to manually open the popup (we want the footer in that case).
323
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_FAILURE;
324
this.defaultIndex = -1;
325
}
326
}
327
328
LoginAutoCompleteResult.prototype = {
329
QueryInterface: ChromeUtils.generateQI([
330
Ci.nsIAutoCompleteResult,
331
Ci.nsISupportsWeakReference,
332
]),
333
334
/**
335
* Accessed via .wrappedJSObject
336
* @private
337
*/
338
get logins() {
339
return this._rows
340
.filter(item => {
341
return item.constructor === LoginAutocompleteItem;
342
})
343
.map(item => item._login);
344
},
345
346
// Allow autoCompleteSearch to get at the JS object so it can
347
// modify some readonly properties for internal use.
348
get wrappedJSObject() {
349
return this;
350
},
351
352
// Interfaces from idl...
353
searchString: null,
354
searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
355
defaultIndex: -1,
356
errorDescription: "",
357
get matchCount() {
358
return this._rows.length;
359
},
360
361
getValueAt(index) {
362
if (index < 0 || index >= this.matchCount) {
363
throw new Error("Index out of range.");
364
}
365
return this._rows[index].value;
366
},
367
368
getLabelAt(index) {
369
if (index < 0 || index >= this.matchCount) {
370
throw new Error("Index out of range.");
371
}
372
return this._rows[index].label;
373
},
374
375
getCommentAt(index) {
376
if (index < 0 || index >= this.matchCount) {
377
throw new Error("Index out of range.");
378
}
379
return this._rows[index].comment;
380
},
381
382
getStyleAt(index) {
383
return this._rows[index].style;
384
},
385
386
getImageAt(index) {
387
return "";
388
},
389
390
getFinalCompleteValueAt(index) {
391
return this.getValueAt(index);
392
},
393
394
removeValueAt(index, removeFromDB) {
395
if (index < 0 || index >= this.matchCount) {
396
throw new Error("Index out of range.");
397
}
398
399
let [removedItem] = this._rows.splice(index, 1);
400
401
if (this.defaultIndex > this._rows.length) {
402
this.defaultIndex--;
403
}
404
405
if (removeFromDB) {
406
removedItem.removeFromStorage();
407
}
408
},
409
};
410
411
function LoginAutoComplete() {}
412
LoginAutoComplete.prototype = {
413
classID: Components.ID("{2bdac17c-53f1-4896-a521-682ccdeef3a8}"),
414
QueryInterface: ChromeUtils.generateQI([Ci.nsILoginAutoCompleteSearch]),
415
416
_autoCompleteLookupPromise: null,
417
418
/**
419
* Yuck. This is called directly by satchel:
420
* nsFormFillController::StartSearch()
421
* [toolkit/components/satchel/nsFormFillController.cpp]
422
*
423
* We really ought to have a simple way for code to register an
424
* auto-complete provider, and not have satchel calling pwmgr directly.
425
*
426
* @param {string} aSearchString The value typed in the field.
427
* @param {nsIAutoCompleteResult} aPreviousResult
428
* @param {HTMLInputElement} aElement
429
* @param {nsIFormAutoCompleteObserver} aCallback
430
*/
431
startSearch(aSearchString, aPreviousResult, aElement, aCallback) {
432
let { isNullPrincipal } = aElement.nodePrincipal;
433
if (aElement.nodePrincipal.schemeIs("about")) {
434
// Don't show autocomplete results for about: pages.
435
return;
436
}
437
438
// Show the insecure login warning in the passwords field on null principal documents.
439
let isSecure = !isNullPrincipal;
440
// Avoid loading InsecurePasswordUtils.jsm in a sandboxed document (e.g. an ad. frame) if we
441
// already know it has a null principal and will therefore get the insecure autocomplete
442
// treatment.
443
// InsecurePasswordUtils doesn't handle the null principal case as not secure because we don't
444
// want the same treatment:
445
// * The web console warnings will be confusing (as they're primarily about http:) and not very
446
// useful if the developer intentionally sandboxed the document.
447
// * The site identity insecure field warning would require LoginManagerChild being loaded and
448
// listening to some of the DOM events we're ignoring in null principal documents. For memory
449
// reasons it's better to not load LMC at all for these sandboxed frames. Also, if the top-
450
// document is sandboxing a document, it probably doesn't want that sandboxed document to be
451
// able to affect the identity icon in the address bar by adding a password field.
452
if (isSecure) {
453
let form = LoginFormFactory.createFromField(aElement);
454
isSecure = InsecurePasswordUtils.isFormSecure(form);
455
}
456
let isPasswordField = aElement.type == "password";
457
let hostname = aElement.ownerDocument.documentURIObject.host;
458
459
let loginManagerActor = LoginManagerChild.forWindow(aElement.ownerGlobal);
460
461
let completeSearch = (
462
autoCompleteLookupPromise,
463
{ generatedPassword, logins, willAutoSaveGeneratedPassword }
464
) => {
465
// If the search was canceled before we got our
466
// results, don't bother reporting them.
467
if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
468
return;
469
}
470
let formOrigin = LoginHelper.getLoginOrigin(
471
aElement.ownerDocument.documentURI
472
);
473
this._autoCompleteLookupPromise = null;
474
let results = new LoginAutoCompleteResult(
475
aSearchString,
476
logins,
477
formOrigin,
478
{
479
generatedPassword,
480
willAutoSaveGeneratedPassword,
481
actor: loginManagerActor,
482
isSecure,
483
isPasswordField,
484
hostname,
485
}
486
);
487
aCallback.onSearchCompletion(results);
488
};
489
490
if (isNullPrincipal) {
491
// Don't search login storage when the field has a null principal as we don't want to fill
492
// logins for the `location` in this case.
493
let acLookupPromise = (this._autoCompleteLookupPromise = Promise.resolve({
494
logins: [],
495
}));
496
acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
497
return;
498
}
499
500
if (
501
isPasswordField &&
502
aSearchString &&
503
!loginManagerActor.isPasswordGenerationForcedOn(aElement)
504
) {
505
// Return empty result on password fields with password already filled,
506
// unless password generation was forced.
507
let acLookupPromise = (this._autoCompleteLookupPromise = Promise.resolve({
508
logins: [],
509
}));
510
acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
511
return;
512
}
513
514
if (!LoginHelper.enabled) {
515
let acLookupPromise = (this._autoCompleteLookupPromise = Promise.resolve({
516
logins: [],
517
}));
518
acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
519
return;
520
}
521
522
log.debug("AutoCompleteSearch invoked. Search is:", aSearchString);
523
524
let previousResult;
525
if (aPreviousResult) {
526
previousResult = {
527
searchString: aPreviousResult.searchString,
528
logins: aPreviousResult.wrappedJSObject.logins,
529
};
530
} else {
531
previousResult = null;
532
}
533
534
let acLookupPromise = (this._autoCompleteLookupPromise = loginManagerActor._autoCompleteSearchAsync(
535
aSearchString,
536
previousResult,
537
aElement
538
));
539
acLookupPromise
540
.then(completeSearch.bind(this, acLookupPromise))
541
.catch(log.error);
542
},
543
544
stopSearch() {
545
this._autoCompleteLookupPromise = null;
546
},
547
};