AboutLoginsParent.jsm 22.2 KB
Newer Older
1
2
3
4
5
6
7
8
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["AboutLoginsParent"];

9
10
11
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
12

13
14
15
16
17
18
19
20
21
XPCOMUtils.defineLazyModuleGetters(this, {
  E10SUtils: "resource://gre/modules/E10SUtils.jsm",
  LoginBreaches: "resource:///modules/LoginBreaches.jsm",
  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
  MigrationUtils: "resource:///modules/MigrationUtils.jsm",
  Services: "resource://gre/modules/Services.jsm",
  UIState: "resource://services-sync/UIState.jsm",
  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
});
22

23
24
25
XPCOMUtils.defineLazyGetter(this, "log", () => {
  return LoginHelper.createLogger("AboutLoginsParent");
});
26
27
28
29
30
31
XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "BREACH_ALERTS_ENABLED",
  "signon.management.page.breach-alerts.enabled",
  false
);
32
33
34
35
36
37
XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "FXA_ENABLED",
  "identity.fxaccounts.enabled",
  false
);
38

39
const ABOUT_LOGINS_ORIGIN = "about:logins";
40
const MASTER_PASSWORD_NOTIFICATION_ID = "master-password-login-required";
41
const PASSWORD_SYNC_NOTIFICATION_ID = "enable-password-sync";
42

43
44
const HIDE_MOBILE_FOOTER_PREF = "signon.management.page.hideMobileFooter";

45
46
47
// about:logins will always use the privileged content process,
// even if it is disabled for other consumers such as about:newtab.
const EXPECTED_ABOUTLOGINS_REMOTE_TYPE = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// App store badges sourced from https://developer.apple.com/app-store/marketing/guidelines/#section-badges.
// This array mirrors the file names from the App store directory (./content/third-party/app-store)
const APP_STORE_LOCALES = [
  "az",
  "ar",
  "bg",
  "cs",
  "da",
  "de",
  "el",
  "en",
  "es-mx",
  "es",
  "et",
  "fi",
  "fr",
  "he",
  "hu",
  "id",
  "it",
  "ja",
  "ko",
  "lt",
  "lv",
  "my",
  "nb",
  "nl",
  "nn",
  "pl",
  "pt-br",
  "pt-pt",
  "ro",
  "ru",
  "si",
  "sk",
  "sv",
  "th",
  "tl",
  "tr",
  "vi",
  "zh-hans",
  "zh-hant",
];

// Google play badges sourced from https://play.google.com/intl/en_us/badges/
// This array mirrors the file names from the play store directory (./content/third-party/play-store)
const PLAY_STORE_LOCALES = [
  "af",
  "ar",
  "az",
  "be",
  "bg",
  "bn",
  "bs",
  "ca",
  "cs",
  "da",
  "de",
  "el",
  "en",
  "es",
  "et",
  "eu",
  "fa",
  "fr",
  "gl",
  "gu",
  "he",
  "hi",
  "hr",
  "hu",
  "hy",
  "id",
  "is",
  "it",
  "ja",
  "ka",
  "kk",
  "km",
  "kn",
  "ko",
  "lo",
  "lt",
  "lv",
  "mk",
  "mr",
  "ms",
  "my",
  "nb",
  "ne",
  "nl",
  "nn",
  "pa",
  "pl",
  "pt-br",
  "pt",
  "ro",
  "ru",
  "si",
  "sk",
  "sl",
  "sq",
  "sr",
  "sv",
  "ta",
  "te",
  "th",
  "tl",
  "tr",
  "uk",
  "ur",
  "uz",
  "vi",
  "zh-cn",
  "zh-tw",
];

166
const convertSubjectToLogin = subject => {
167
168
  subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
  const login = LoginHelper.loginToVanillaObject(subject);
169
  if (!LoginHelper.isUserFacingLogin(login)) {
170
171
172
173
174
    return null;
  }
  return augmentVanillaLoginObject(login);
};

175
const SUBDOMAIN_REGEX = new RegExp(/^www\d*\./);
176
const augmentVanillaLoginObject = login => {
177
178
  // Note that `displayOrigin` can also include a httpRealm.
  let title = login.displayOrigin.replace(SUBDOMAIN_REGEX, "");
179
180
181
  return Object.assign({}, login, {
    title,
  });
182
183
184
};

var AboutLoginsParent = {
185
  _l10n: null,
186
  _subscribers: new WeakSet(),
187
  _observersAdded: false,
188
189

  // Listeners are added in BrowserGlue.jsm
190
  async receiveMessage(message) {
191
192
193
194
195
196
197
198
199
200
201
    // Only respond to messages sent from a privlegedabout process. Ideally
    // we would also check the contentPrincipal.originNoSuffix but this
    // check has been removed due to bug 1576722.
    if (message.target.remoteType != EXPECTED_ABOUTLOGINS_REMOTE_TYPE) {
      throw new Error(
        `AboutLoginsParent: Received ${
          message.name
        } message the remote type didn't match expectations: ${
          message.target.remoteType
        } == ${EXPECTED_ABOUTLOGINS_REMOTE_TYPE}`
      );
202
203
    }

204
    this._subscribers.add(message.target);
205
    switch (message.name) {
206
207
      case "AboutLogins:CreateLogin": {
        let newLogin = message.data.login;
208
209
210
        // Remove the path from the origin, if it was provided.
        let origin = LoginHelper.getLoginOrigin(newLogin.origin);
        if (!origin) {
211
212
213
          Cu.reportError(
            "AboutLogins:CreateLogin: Unable to get an origin from the login details."
          );
214
215
216
          return;
        }
        newLogin.origin = origin;
217
        Object.assign(newLogin, {
218
          formActionOrigin: "",
219
220
221
          usernameField: "",
          passwordField: "",
        });
222
223
224
225
226
227
        newLogin = LoginHelper.vanillaObjectToLogin(newLogin);
        try {
          Services.logins.addLogin(newLogin);
        } catch (error) {
          this.handleLoginStorageErrors(newLogin, error, message);
        }
228
229
        break;
      }
230
231
232
233
      case "AboutLogins:DeleteLogin": {
        let login = LoginHelper.vanillaObjectToLogin(message.data.login);
        Services.logins.removeLogin(login);
        break;
234
      }
235
236
237
      case "AboutLogins:DismissBreachAlert": {
        const login = message.data.login;

238
        await LoginBreaches.recordDismissal(login.guid);
239
        const logins = await this.getAllLogins();
240
        const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
241
242
243
244
          logins
        );
        const messageManager = message.target.messageManager;
        messageManager.sendAsyncMessage(
245
          "AboutLogins:SetBreaches",
246
247
248
249
          breachesByLoginGUID
        );
        break;
      }
250
251
252
253
      case "AboutLogins:HideFooter": {
        Services.prefs.setBoolPref(HIDE_MOBILE_FOOTER_PREF, true);
        break;
      }
254
255
256
257
258
259
260
261
262
263
      case "AboutLogins:SyncEnable": {
        message.target.ownerGlobal.gSync.openFxAEmailFirstPage(
          "password-manager"
        );
        break;
      }
      case "AboutLogins:SyncOptions": {
        message.target.ownerGlobal.gSync.openFxAManagePage("password-manager");
        break;
      }
264
265
      case "AboutLogins:Import": {
        try {
266
267
268
          MigrationUtils.showMigrationWizard(message.target.ownerGlobal, [
            MigrationUtils.MIGRATION_ENTRYPOINT_PASSWORDS,
          ]);
269
270
271
272
        } catch (ex) {
          Cu.reportError(ex);
        }
        break;
273
      }
274
275
276
277
278
      case "AboutLogins:GetHelp": {
        const SUPPORT_URL =
          Services.urlFormatter.formatURLPref("app.support.baseURL") +
          "firefox-lockwise";
        message.target.ownerGlobal.openWebLinkIn(SUPPORT_URL, "tab", {
279
280
          relatedToCurrent: true,
        });
281
        break;
282
      }
283
284
285
      case "AboutLogins:OpenMobileAndroid": {
        const MOBILE_ANDROID_URL_PREF =
          "signon.management.page.mobileAndroidURL";
286
287
        const linkTrackingSource = message.data.source;
        let MOBILE_ANDROID_URL = Services.prefs.getStringPref(
288
289
          MOBILE_ANDROID_URL_PREF
        );
290
291
        // Append the `utm_creative` query parameter value:
        MOBILE_ANDROID_URL += linkTrackingSource;
292
293
294
295
296
297
298
        message.target.ownerGlobal.openWebLinkIn(MOBILE_ANDROID_URL, "tab", {
          relatedToCurrent: true,
        });
        break;
      }
      case "AboutLogins:OpenMobileIos": {
        const MOBILE_IOS_URL_PREF = "signon.management.page.mobileAppleURL";
299
300
301
302
        const linkTrackingSource = message.data.source;
        let MOBILE_IOS_URL = Services.prefs.getStringPref(MOBILE_IOS_URL_PREF);
        // Append the `utm_creative` query parameter value:
        MOBILE_IOS_URL += linkTrackingSource;
303
304
305
306
307
        message.target.ownerGlobal.openWebLinkIn(MOBILE_IOS_URL, "tab", {
          relatedToCurrent: true,
        });
        break;
      }
308
309
310
311
      case "AboutLogins:OpenPreferences": {
        message.target.ownerGlobal.openPreferences("privacy-logins");
        break;
      }
312
313
      case "AboutLogins:OpenSite": {
        let guid = message.data.login.guid;
314
        let logins = LoginHelper.searchLoginsWithObject({ guid });
315
        if (logins.length != 1) {
316
317
318
319
320
          log.warn(
            `AboutLogins:OpenSite: expected to find a login for guid: ${guid} but found ${
              logins.length
            }`
          );
321
322
323
          return;
        }

324
325
326
        message.target.ownerGlobal.openWebLinkIn(logins[0].origin, "tab", {
          relatedToCurrent: true,
        });
327
328
        break;
      }
329
      case "AboutLogins:MasterPasswordRequest": {
330
        // This does no harm if master password isn't set.
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
        let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(
          Ci.nsIPK11TokenDB
        );
        let token = tokendb.getInternalKeyToken();

        let messageManager = message.target.messageManager;

        // If there is no master password, return as-if authentication succeeded.
        if (token.checkPassword("")) {
          messageManager.sendAsyncMessage(
            "AboutLogins:MasterPasswordResponse",
            true
          );
          return;
        }

        // If a master password prompt is already open, just exit early and return false.
        // The user can re-trigger it after responding to the already open dialog.
        if (Services.logins.uiBusy) {
          messageManager.sendAsyncMessage(
            "AboutLogins:MasterPasswordResponse",
            false
          );
          return;
        }

        // So there's a master password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl).
        try {
          // Relogin and ask for the master password.
          token.login(true); // 'true' means always prompt for token password. User will be prompted until
          // clicking 'Cancel' or entering the correct password.
        } catch (e) {
          // An exception will be thrown if the user cancels the login prompt dialog.
          // User is also logged out of Software Security Device.
        }

        messageManager.sendAsyncMessage(
          "AboutLogins:MasterPasswordResponse",
          token.isLoggedIn()
        );
        break;
      }
373
      case "AboutLogins:Subscribe": {
374
        if (!this._observersAdded) {
375
          Services.obs.addObserver(this, "passwordmgr-crypto-login");
376
          Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled");
377
          Services.obs.addObserver(this, "passwordmgr-storage-changed");
378
          Services.obs.addObserver(this, UIState.ON_UPDATE);
379
          this._observersAdded = true;
380
381
        }
        let messageManager = message.target.messageManager;
382
383

        const logins = await this.getAllLogins();
384
        try {
385
          let syncState = this.getSyncState();
386
          if (FXA_ENABLED) {
387
            this.updatePasswordSyncNotificationState(syncState);
388
          }
389
390
391

          const playStoreBadgeLanguage = Services.locale.negotiateLanguages(
            Services.locale.appLocalesAsBCP47,
392
393
            PLAY_STORE_LOCALES,
            "en-us",
394
            Services.locale.langNegStrategyLookup
395
          )[0];
396
397
398

          const appStoreBadgeLanguage = Services.locale.negotiateLanguages(
            Services.locale.appLocalesAsBCP47,
399
400
            APP_STORE_LOCALES,
            "en-us",
401
            Services.locale.langNegStrategyLookup
402
          )[0];
403
404

          const selectedBadgeLanguages = {
405
406
            appStoreBadgeLanguage,
            playStoreBadgeLanguage,
407
408
          };

409
410
411
412
          messageManager.sendAsyncMessage("AboutLogins:Setup", {
            logins,
            syncState,
            selectedBadgeLanguages,
413
            masterPasswordEnabled: LoginHelper.isMasterPasswordSet(),
414
          });
415

416
          await this._sendAllLoginRelatedObjects(logins, messageManager);
417
418
419
420
421
422
423
424
425
426
427
        } catch (ex) {
          if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
            throw ex;
          }

          // The message manager may be destroyed before the replies can be sent.
          log.debug(
            "AboutLogins:Subscribe: exception when replying with logins",
            ex
          );
        }
428
429
        break;
      }
430
431
      case "AboutLogins:UpdateLogin": {
        let loginUpdates = message.data.login;
432
433
434
        let logins = LoginHelper.searchLoginsWithObject({
          guid: loginUpdates.guid,
        });
435
        if (logins.length != 1) {
436
437
438
439
440
          log.warn(
            `AboutLogins:UpdateLogin: expected to find a login for guid: ${
              loginUpdates.guid
            } but found ${logins.length}`
          );
441
442
443
444
445
446
447
448
449
450
          return;
        }

        let modifiedLogin = logins[0].clone();
        if (loginUpdates.hasOwnProperty("username")) {
          modifiedLogin.username = loginUpdates.username;
        }
        if (loginUpdates.hasOwnProperty("password")) {
          modifiedLogin.password = loginUpdates.password;
        }
451
452
453
454
455
        try {
          Services.logins.modifyLogin(logins[0], modifiedLogin);
        } catch (error) {
          this.handleLoginStorageErrors(modifiedLogin, error, message);
        }
456
457
        break;
      }
458
459
460
    }
  },

461
  handleLoginStorageErrors(login, error, message) {
462
    let messageObject = {
463
      login: augmentVanillaLoginObject(LoginHelper.loginToVanillaObject(login)),
464
465
466
467
468
469
470
471
472
473
474
475
476
477
      errorMessage: error.message,
    };

    if (error.message.includes("This login already exists")) {
      // See comment in LoginHelper.createLoginAlreadyExistsError as to
      // why we need to call .toString() on the nsISupportsString.
      messageObject.existingLoginGuid = error.data.toString();
    }

    const messageManager = message.target.messageManager;
    messageManager.sendAsyncMessage(
      "AboutLogins:ShowLoginItemError",
      messageObject
    );
478
479
  },

480
  async observe(subject, topic, type) {
481
    if (!ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers).length) {
482
      Services.obs.removeObserver(this, "passwordmgr-crypto-login");
483
      Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
484
      Services.obs.removeObserver(this, "passwordmgr-storage-changed");
485
      Services.obs.removeObserver(this, UIState.ON_UPDATE);
486
      this._observersAdded = false;
487
488
489
      return;
    }

490
    if (topic == "passwordmgr-crypto-login") {
491
      this.removeNotifications(MASTER_PASSWORD_NOTIFICATION_ID);
492
493
494
      let logins = await this.getAllLogins();
      this.messageSubscribers("AboutLogins:AllLogins", logins);
      await this._sendAllLoginRelatedObjects(logins);
495
496
497
      return;
    }

498
499
500
501
502
    if (topic == "passwordmgr-crypto-loginCanceled") {
      this.showMasterPasswordLoginNotifications();
      return;
    }

503
504
505
506
507
    if (topic == UIState.ON_UPDATE) {
      this.messageSubscribers("AboutLogins:SyncState", this.getSyncState());
      return;
    }

508
509
510
511
512
513
514
    switch (type) {
      case "addLogin": {
        const login = convertSubjectToLogin(subject);
        if (!login) {
          return;
        }
        this.messageSubscribers("AboutLogins:LoginAdded", login);
515
516
517
518
519
520
521

        if (BREACH_ALERTS_ENABLED) {
          this.messageSubscribers(
            "AboutLogins:UpdateBreaches",
            await LoginBreaches.getPotentialBreachesByLoginGUID([login])
          );
        }
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
        break;
      }
      case "modifyLogin": {
        subject.QueryInterface(Ci.nsIArrayExtensions);
        const login = convertSubjectToLogin(subject.GetElementAt(1));
        if (!login) {
          return;
        }
        this.messageSubscribers("AboutLogins:LoginModified", login);
        break;
      }
      case "removeLogin": {
        const login = convertSubjectToLogin(subject);
        if (!login) {
          return;
        }
        this.messageSubscribers("AboutLogins:LoginRemoved", login);
        break;
      }
541
542
543
544
      case "removeAllLogins": {
        this.messageSubscribers("AboutLogins:AllLogins", []);
        break;
      }
545
546
547
    }
  },

548
549
  async getFavicon(login) {
    try {
550
      const faviconData = await PlacesUtils.promiseFaviconData(login.origin);
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
      return {
        faviconData,
        guid: login.guid,
      };
    } catch (ex) {
      return null;
    }
  },

  async getAllFavicons(logins) {
    let favicons = await Promise.all(
      logins.map(login => this.getFavicon(login))
    );
    let vanillaFavicons = {};
    for (let favicon of favicons) {
      if (!favicon) {
        continue;
      }
      try {
        vanillaFavicons[favicon.guid] = {
          data: favicon.faviconData.data,
          dataLen: favicon.faviconData.dataLen,
          mimeType: favicon.faviconData.mimeType,
        };
      } catch (ex) {}
    }
    return vanillaFavicons;
  },

580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
  showMasterPasswordLoginNotifications() {
    this.showNotifications({
      id: MASTER_PASSWORD_NOTIFICATION_ID,
      priority: "PRIORITY_WARNING_MEDIUM",
      iconURL: "chrome://browser/skin/login.svg",
      messageId: "master-password-notification-message",
      buttonId: "master-password-reload-button",
      onClick(browser) {
        browser.reload();
      },
    });
  },

  showPasswordSyncNotifications() {
    this.showNotifications({
      id: PASSWORD_SYNC_NOTIFICATION_ID,
      priority: "PRIORITY_INFO_MEDIUM",
      iconURL: "chrome://browser/skin/login.svg",
      messageId: "enable-password-sync-notification-message",
      buttonId: "enable-password-sync-preferences-button",
      onClick(browser) {
601
        browser.ownerGlobal.gSync.openPrefs("password-manager");
602
603
604
605
606
607
608
609
610
611
612
613
614
615
      },
      extraFtl: ["branding/brand.ftl", "browser/branding/sync-brand.ftl"],
    });
  },

  showNotifications({
    id,
    priority,
    iconURL,
    messageId,
    buttonId,
    onClick,
    extraFtl = [],
  } = {}) {
616
    for (let subscriber of this._subscriberIterator()) {
617
618
      let MozXULElement = subscriber.ownerGlobal.MozXULElement;
      MozXULElement.insertFTLIfNeeded("browser/aboutLogins.ftl");
619
620
621
      for (let ftl of extraFtl) {
        MozXULElement.insertFTLIfNeeded(ftl);
      }
622

623
      // If there's already an existing notification bar, don't do anything.
624
      let { gBrowser } = subscriber.ownerGlobal;
625
626
      let browser = subscriber;
      let notificationBox = gBrowser.getNotificationBox(browser);
627
      let notification = notificationBox.getNotificationWithValue(id);
628
629
630
631
632
      if (notification) {
        continue;
      }

      // Configure the notification bar
633
634
635
      let doc = subscriber.ownerDocument;
      let messageFragment = doc.createDocumentFragment();
      let message = doc.createElement("span");
636
      doc.l10n.setAttributes(message, messageId);
637
      messageFragment.appendChild(message);
638

639
640
      let buttons = [
        {
641
          "l10n-id": buttonId,
642
          popup: null,
643
644
          callback: () => {
            onClick(browser);
645
646
647
648
649
650
          },
        },
      ];

      notification = notificationBox.appendNotification(
        messageFragment,
651
        id,
652
        iconURL,
653
        notificationBox[priority],
654
655
        buttons
      );
656
657
658
    }
  },

659
  removeNotifications(notificationId) {
660
    for (let subscriber of this._subscriberIterator()) {
661
      let { gBrowser } = subscriber.ownerGlobal;
662
663
      let browser = subscriber;
      let notificationBox = gBrowser.getNotificationBox(browser);
664
      let notification = notificationBox.getNotificationWithValue(
665
        notificationId
666
      );
667
668
669
670
671
672
673
      if (!notification) {
        continue;
      }
      notificationBox.removeNotification(notification);
    }
  },

674
675
676
677
  *_subscriberIterator() {
    let subscribers = ChromeUtils.nondeterministicGetWeakSetKeys(
      this._subscribers
    );
678
    for (let subscriber of subscribers) {
679
680
681
682
683
      if (
        subscriber.remoteType != EXPECTED_ABOUTLOGINS_REMOTE_TYPE ||
        !subscriber.contentPrincipal ||
        subscriber.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN
      ) {
684
685
686
        this._subscribers.delete(subscriber);
        continue;
      }
687
688
689
690
691
692
      yield subscriber;
    }
  },

  messageSubscribers(name, details) {
    for (let subscriber of this._subscriberIterator()) {
693
694
      try {
        subscriber.messageManager.sendAsyncMessage(name, details);
695
696
697
698
699
700
701
702
703
704
705
      } catch (ex) {
        if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
          throw ex;
        }

        // The message manager may be destroyed before the message is sent.
        log.debug(
          "messageSubscribers: exception when calling sendAsyncMessage",
          ex
        );
      }
706
707
708
    }
  },

709
710
  async getAllLogins() {
    try {
711
      let logins = await LoginHelper.getAllUserFacingLogins();
712
713
714
      return logins
        .map(LoginHelper.loginToVanillaObject)
        .map(augmentVanillaLoginObject);
715
716
717
718
719
720
721
    } catch (e) {
      if (e.result == Cr.NS_ERROR_ABORT) {
        // If the user cancels the MP prompt then return no logins.
        return [];
      }
      throw e;
    }
722
  },
723

724
725
726
727
728
729
730
731
732
733
  async _sendAllLoginRelatedObjects(logins, messageManager) {
    let sendMessageFn = (name, details) => {
      if (messageManager) {
        messageManager.sendAsyncMessage(name, details);
      } else {
        this.messageSubscribers(name, details);
      }
    };
    if (BREACH_ALERTS_ENABLED) {
      sendMessageFn(
734
        "AboutLogins:SetBreaches",
735
736
737
738
739
740
741
742
743
744
        await LoginBreaches.getPotentialBreachesByLoginGUID(logins)
      );
    }

    sendMessageFn(
      "AboutLogins:SendFavicons",
      await this.getAllFavicons(logins)
    );
  },

745
746
747
748
749
750
  getSyncState() {
    const state = UIState.get();
    // As long as Sync is configured, about:logins will treat it as
    // authenticated. More diagnostics and error states can be handled
    // by other more Sync-specific pages.
    const loggedIn = state.status != UIState.STATUS_NOT_CONFIGURED;
751
752
753
754
755
756

    // Pass the pref set if user has dismissed mobile promo footer
    const dismissedMobileFooter = Services.prefs.getBoolPref(
      HIDE_MOBILE_FOOTER_PREF
    );

757
758
759
760
    return {
      loggedIn,
      email: state.email,
      avatarURL: state.avatarURL,
761
      hideMobileFooter: !loggedIn || dismissedMobileFooter,
762
      fxAccountsEnabled: FXA_ENABLED,
763
764
765
    };
  },

766
767
  updatePasswordSyncNotificationState(
    syncState,
768
769
    // Need to explicitly call the getter on lazy preference getters
    // to activate their observer.
770
771
772
    passwordSyncEnabled = PASSWORD_SYNC_ENABLED
  ) {
    if (syncState.loggedIn && !passwordSyncEnabled) {
773
774
775
776
777
      this.showPasswordSyncNotifications();
      return;
    }
    this.removeNotifications(PASSWORD_SYNC_NOTIFICATION_ID);
  },
778
779
780
781

  onPasswordSyncEnabledPreferenceChange(data, previous, latest) {
    this.updatePasswordSyncNotificationState(this.getSyncState(), latest);
  },
782
};
783
784
785
786
787
788

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "PASSWORD_SYNC_ENABLED",
  "services.sync.engine.passwords",
  false,
789
790
791
  AboutLoginsParent.onPasswordSyncEnabledPreferenceChange.bind(
    AboutLoginsParent
  )
792
);