tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

AccountsGlue.sys.mjs (15468B)


      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 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 const lazy = {};
      9 
     10 ChromeUtils.defineESModuleGetters(lazy, {
     11  AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
     12  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     13  BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
     14  ClientID: "resource://gre/modules/ClientID.sys.mjs",
     15  CloseRemoteTab: "resource://gre/modules/FxAccountsCommands.sys.mjs",
     16  FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
     17  UIState: "resource://services-sync/UIState.sys.mjs",
     18 });
     19 
     20 XPCOMUtils.defineLazyPreferenceGetter(
     21  lazy,
     22  "CLIENT_ASSOCIATION_PING_ENABLED",
     23  "identity.fxaccounts.telemetry.clientAssociationPing.enabled",
     24  false
     25 );
     26 
     27 XPCOMUtils.defineLazyServiceGetter(
     28  lazy,
     29  "AlertsService",
     30  "@mozilla.org/alerts-service;1",
     31  Ci.nsIAlertsService
     32 );
     33 
     34 ChromeUtils.defineLazyGetter(
     35  lazy,
     36  "accountsL10n",
     37  () => new Localization(["browser/accounts.ftl", "branding/brand.ftl"], true)
     38 );
     39 
     40 const AlertNotification = Components.Constructor(
     41  "@mozilla.org/alert-notification;1",
     42  "nsIAlertNotification",
     43  "initWithObject"
     44 );
     45 
     46 /**
     47 * Manages Mozilla Account and Sync related functionality
     48 * needed at startup. It mainly handles various account-related events and notifications.
     49 *
     50 * This module was sliced off of BrowserGlue and designed to centralize
     51 * account-related events/notifications to prevent crowding BrowserGlue
     52 */
     53 export const AccountsGlue = {
     54  QueryInterface: ChromeUtils.generateQI([
     55    "nsIObserver",
     56    "nsISupportsWeakReference",
     57  ]),
     58 
     59  init() {
     60    let os = Services.obs;
     61    [
     62      "fxaccounts:onverified",
     63      "fxaccounts:device_connected",
     64      "fxaccounts:verify_login",
     65      "fxaccounts:device_disconnected",
     66      "fxaccounts:commands:open-uri",
     67      "fxaccounts:commands:close-uri",
     68      "sync-ui-state:update",
     69    ].forEach(topic => os.addObserver(this, topic, true));
     70  },
     71 
     72  observe(subject, topic, data) {
     73    switch (topic) {
     74      case "fxaccounts:onverified":
     75        this._onThisDeviceConnected();
     76        break;
     77      case "fxaccounts:device_connected":
     78        this._onDeviceConnected(data);
     79        break;
     80      case "fxaccounts:verify_login":
     81        this._onVerifyLoginNotification(JSON.parse(data));
     82        break;
     83      case "fxaccounts:device_disconnected":
     84        data = JSON.parse(data);
     85        if (data.isLocalDevice) {
     86          this._onDeviceDisconnected();
     87        }
     88        break;
     89      case "fxaccounts:commands:open-uri":
     90        this._onDisplaySyncURIs(subject);
     91        break;
     92      case "fxaccounts:commands:close-uri":
     93        this._onIncomingCloseTabCommand(subject);
     94        break;
     95      case "sync-ui-state:update": {
     96        this._updateFxaBadges(
     97          lazy.BrowserWindowTracker.getTopWindow({
     98            allowFromInactiveWorkspace: true,
     99          })
    100        );
    101 
    102        if (lazy.CLIENT_ASSOCIATION_PING_ENABLED) {
    103          let fxaState = lazy.UIState.get();
    104          if (fxaState.status == lazy.UIState.STATUS_SIGNED_IN) {
    105            Glean.clientAssociation.uid.set(fxaState.uid);
    106            Glean.clientAssociation.legacyClientId.set(
    107              lazy.ClientID.getCachedClientID()
    108            );
    109          }
    110        }
    111        break;
    112      }
    113      case "browser-glue-test": // used by tests
    114        if (data == "mock-alerts-service") {
    115          // eslint-disable-next-line mozilla/valid-lazy
    116          Object.defineProperty(lazy, "AlertsService", {
    117            value: subject.wrappedJSObject,
    118          });
    119        }
    120        break;
    121    }
    122  },
    123 
    124  _onThisDeviceConnected() {
    125    const [title, body] = lazy.accountsL10n.formatValuesSync([
    126      "account-connection-title-2",
    127      "account-connection-connected",
    128    ]);
    129 
    130    let clickCallback = (subject, topic) => {
    131      if (topic != "alertclickcallback") {
    132        return;
    133      }
    134      this._openPreferences("sync");
    135    };
    136    let alert = new AlertNotification({
    137      title,
    138      text: body,
    139      textClickable: true,
    140    });
    141    lazy.AlertsService.showAlert(alert, clickCallback);
    142  },
    143 
    144  _openURLInNewWindow(url) {
    145    let urlString = Cc["@mozilla.org/supports-string;1"].createInstance(
    146      Ci.nsISupportsString
    147    );
    148    urlString.data = url;
    149    return new Promise(resolve => {
    150      let win = Services.ww.openWindow(
    151        null,
    152        AppConstants.BROWSER_CHROME_URL,
    153        "_blank",
    154        "chrome,all,dialog=no",
    155        urlString
    156      );
    157      win.addEventListener(
    158        "load",
    159        () => {
    160          resolve(win);
    161        },
    162        { once: true }
    163      );
    164    });
    165  },
    166 
    167  /**
    168   * Called as an observer when Sync's "display URIs" notification is fired.
    169   * We open the received URIs in background tabs.
    170   *
    171   * @param {object} data
    172   *        The data passed to the observer notification, which contains
    173   *        a wrappedJSObject with the URIs to open.
    174   */
    175  async _onDisplaySyncURIs(data) {
    176    try {
    177      // The payload is wrapped weirdly because of how Sync does notifications.
    178      const URIs = data.wrappedJSObject.object;
    179 
    180      // win can be null, but it's ok, we'll assign it later in openTab()
    181      let win = lazy.BrowserWindowTracker.getTopWindow({ private: false });
    182 
    183      const openTab = async URI => {
    184        let tab;
    185        if (!win) {
    186          win = await this._openURLInNewWindow(URI.uri);
    187          let tabs = win.gBrowser.tabs;
    188          tab = tabs[tabs.length - 1];
    189        } else {
    190          tab = win.gBrowser.addWebTab(URI.uri);
    191        }
    192        tab.attention = true;
    193        return tab;
    194      };
    195 
    196      const firstTab = await openTab(URIs[0]);
    197      await Promise.all(URIs.slice(1).map(URI => openTab(URI)));
    198 
    199      const deviceName = URIs[0].sender && URIs[0].sender.name;
    200      let titleL10nId, body;
    201      if (URIs.length == 1) {
    202        // Due to bug 1305895, tabs from iOS may not have device information, so
    203        // we have separate strings to handle those cases. (See Also
    204        // unnamedTabsArrivingNotificationNoDevice.body below)
    205        titleL10nId = deviceName
    206          ? {
    207              id: "account-single-tab-arriving-from-device-title",
    208              args: { deviceName },
    209            }
    210          : { id: "account-single-tab-arriving-title" };
    211        // Use the page URL as the body. We strip the fragment and query (after
    212        // the `?` and `#` respectively) to reduce size, and also format it the
    213        // same way that the url bar would.
    214        let url = URIs[0].uri.replace(/([?#]).*$/, "$1");
    215        const wasTruncated = url.length < URIs[0].uri.length;
    216        url = lazy.BrowserUIUtils.trimURL(url);
    217        if (wasTruncated) {
    218          body = await lazy.accountsL10n.formatValue(
    219            "account-single-tab-arriving-truncated-url",
    220            { url }
    221          );
    222        } else {
    223          body = url;
    224        }
    225      } else {
    226        titleL10nId = { id: "account-multiple-tabs-arriving-title" };
    227        const allKnownSender = URIs.every(URI => URI.sender != null);
    228        const allSameDevice =
    229          allKnownSender &&
    230          URIs.every(URI => URI.sender.id == URIs[0].sender.id);
    231        let bodyL10nId;
    232        if (allSameDevice) {
    233          bodyL10nId = deviceName
    234            ? "account-multiple-tabs-arriving-from-single-device"
    235            : "account-multiple-tabs-arriving-from-unknown-device";
    236        } else {
    237          bodyL10nId = "account-multiple-tabs-arriving-from-multiple-devices";
    238        }
    239 
    240        body = await lazy.accountsL10n.formatValue(bodyL10nId, {
    241          deviceName,
    242          tabCount: URIs.length,
    243        });
    244      }
    245      const title = await lazy.accountsL10n.formatValue(titleL10nId);
    246 
    247      const clickCallback = (obsSubject, obsTopic) => {
    248        if (obsTopic == "alertclickcallback") {
    249          win.gBrowser.selectedTab = firstTab;
    250        }
    251      };
    252 
    253      let alert = new AlertNotification({
    254        title,
    255        text: body,
    256        textClickable: true,
    257      });
    258      lazy.AlertsService.showAlert(alert, clickCallback);
    259    } catch (ex) {
    260      console.error("Error displaying tab(s) received by Sync: ", ex);
    261    }
    262  },
    263 
    264  async _onIncomingCloseTabCommand(data) {
    265    // The payload is wrapped weirdly because of how Sync does notifications.
    266    const wrappedObj = data.wrappedJSObject.object;
    267    let { urls } = wrappedObj[0];
    268    let urisToClose = [];
    269    urls.forEach(urlString => {
    270      try {
    271        urisToClose.push(Services.io.newURI(urlString));
    272      } catch (ex) {
    273        // The url was invalid so we ignore
    274        console.error(ex);
    275      }
    276    });
    277    // We want to keep track of the tabs we closed for the notification
    278    // given that there could be duplicates we also closed
    279    let totalClosedTabs = 0;
    280    const windows = lazy.BrowserWindowTracker.orderedWindows;
    281 
    282    async function closeTabsInWindows() {
    283      for (const win of windows) {
    284        if (!win.gBrowser) {
    285          continue;
    286        }
    287        try {
    288          const closedInWindow = await win.gBrowser.closeTabsByURI(urisToClose);
    289          totalClosedTabs += closedInWindow;
    290        } catch (ex) {
    291          this.log.error("Error closing tabs in window:", ex);
    292        }
    293      }
    294    }
    295 
    296    await closeTabsInWindows();
    297 
    298    let clickCallback = async (subject, topic) => {
    299      if (topic == "alertshow") {
    300        // Keep track of the fact that we showed the notification to
    301        // the user at least once
    302        lazy.CloseRemoteTab.hasPendingCloseTabNotification = true;
    303      }
    304 
    305      // The notification is either turned off or dismissed by user
    306      if (topic == "alertfinished") {
    307        // Reset the notification pending flag
    308        lazy.CloseRemoteTab.hasPendingCloseTabNotification = false;
    309      }
    310 
    311      if (topic != "alertclickcallback") {
    312        return;
    313      }
    314      let win =
    315        lazy.BrowserWindowTracker.getTopWindow({ private: false }) ??
    316        (await lazy.BrowserWindowTracker.promiseOpenWindow());
    317      // We don't want to open a new tab, instead use the handler
    318      // to switch to the existing view
    319      if (win) {
    320        win.FirefoxViewHandler.openTab("recentlyclosed");
    321      }
    322    };
    323 
    324    // Reset the count only if there are no pending notifications
    325    if (!lazy.CloseRemoteTab.hasPendingCloseTabNotification) {
    326      lazy.CloseRemoteTab.closeTabNotificationCount = 0;
    327    }
    328    lazy.CloseRemoteTab.closeTabNotificationCount += totalClosedTabs;
    329    const [title, body] = await lazy.accountsL10n.formatValues([
    330      {
    331        id: "account-tabs-closed-remotely",
    332        args: { closedCount: lazy.CloseRemoteTab.closeTabNotificationCount },
    333      },
    334      { id: "account-view-recently-closed-tabs" },
    335    ]);
    336 
    337    try {
    338      let alert = new AlertNotification({
    339        title,
    340        text: body,
    341        textClickable: true,
    342        name: "closed-tab-notification",
    343      });
    344      lazy.AlertsService.showAlert(alert, clickCallback);
    345    } catch (ex) {
    346      console.error("Error notifying user of closed tab(s) ", ex);
    347    }
    348  },
    349 
    350  async _onVerifyLoginNotification({ body, title, url }) {
    351    let tab;
    352    let win = lazy.BrowserWindowTracker.getTopWindow({ private: false });
    353    if (!win) {
    354      win = await this._openURLInNewWindow(url);
    355      let tabs = win.gBrowser.tabs;
    356      tab = tabs[tabs.length - 1];
    357    } else {
    358      tab = win.gBrowser.addWebTab(url);
    359    }
    360    tab.attention = true;
    361    let clickCallback = (subject, topic) => {
    362      if (topic != "alertclickcallback") {
    363        return;
    364      }
    365      win.gBrowser.selectedTab = tab;
    366    };
    367 
    368    try {
    369      let alert = new AlertNotification({
    370        title,
    371        body,
    372        textClickable: true,
    373      });
    374      lazy.AlertsService.showAlert(alert, clickCallback);
    375    } catch (ex) {
    376      console.error("Error notifying of a verify login event: ", ex);
    377    }
    378  },
    379 
    380  _onDeviceConnected(deviceName) {
    381    const [title, body] = lazy.accountsL10n.formatValuesSync([
    382      { id: "account-connection-title-2" },
    383      deviceName
    384        ? { id: "account-connection-connected-with", args: { deviceName } }
    385        : { id: "account-connection-connected-with-noname" },
    386    ]);
    387 
    388    let clickCallback = async (subject, topic) => {
    389      if (topic != "alertclickcallback") {
    390        return;
    391      }
    392      let url = await lazy.FxAccounts.config.promiseManageDevicesURI(
    393        "device-connected-notification"
    394      );
    395      let win = lazy.BrowserWindowTracker.getTopWindow({ private: false });
    396      if (!win) {
    397        this._openURLInNewWindow(url);
    398      } else {
    399        win.gBrowser.addWebTab(url);
    400      }
    401    };
    402 
    403    try {
    404      let alert = new AlertNotification({
    405        title,
    406        text: body,
    407        textClickable: true,
    408      });
    409      lazy.AlertsService.showAlert(alert, clickCallback);
    410    } catch (ex) {
    411      console.error("Error notifying of a new Sync device: ", ex);
    412    }
    413  },
    414 
    415  _onDeviceDisconnected() {
    416    const [title, body] = lazy.accountsL10n.formatValuesSync([
    417      "account-connection-title-2",
    418      "account-connection-disconnected",
    419    ]);
    420 
    421    let clickCallback = (subject, topic) => {
    422      if (topic != "alertclickcallback") {
    423        return;
    424      }
    425      this._openPreferences("sync");
    426    };
    427 
    428    let alert = new AlertNotification({
    429      title,
    430      text: body,
    431      textClickable: true,
    432    });
    433    lazy.AlertsService.showAlert(alert, clickCallback);
    434  },
    435 
    436  _updateFxaBadges(win) {
    437    let fxaButton = win.document.getElementById("fxa-toolbar-menu-button");
    438    let badge = fxaButton?.querySelector(".toolbarbutton-badge");
    439 
    440    let state = lazy.UIState.get();
    441    if (
    442      state.status == lazy.UIState.STATUS_LOGIN_FAILED ||
    443      state.status == lazy.UIState.STATUS_NOT_VERIFIED
    444    ) {
    445      // If the fxa toolbar button is in the toolbox, we display the notification
    446      // on the fxa button instead of the app menu.
    447      let navToolbox = win.document.getElementById("navigator-toolbox");
    448      let isFxAButtonShown = navToolbox.contains(fxaButton);
    449      if (isFxAButtonShown) {
    450        state.status == lazy.UIState.STATUS_LOGIN_FAILED
    451          ? fxaButton?.setAttribute("badge-status", state.status)
    452          : badge?.classList.add("feature-callout");
    453      } else {
    454        lazy.AppMenuNotifications.showBadgeOnlyNotification(
    455          "fxa-needs-authentication"
    456        );
    457      }
    458    } else {
    459      fxaButton?.removeAttribute("badge-status");
    460      badge?.classList.remove("feature-callout");
    461      lazy.AppMenuNotifications.removeNotification("fxa-needs-authentication");
    462    }
    463  },
    464 
    465  // Open preferences even if there are no open windows.
    466  async _openPreferences(...args) {
    467    let chromeWindow = lazy.BrowserWindowTracker.getTopWindow();
    468    if (!chromeWindow && AppConstants.platform !== "macosx") {
    469      // If we're not on macOS, there may be no windows open in this
    470      // workspace, so open a new one. (the macOS case is handled below)
    471      //
    472      // This should get cleaned up in bug 1983081 since openPreferences()
    473      // shouldn't require a window argument.
    474      chromeWindow = await lazy.BrowserWindowTracker.promiseOpenWindow();
    475    }
    476    if (chromeWindow) {
    477      chromeWindow.openPreferences(...args);
    478      return;
    479    }
    480 
    481    if (AppConstants.platform == "macosx") {
    482      Services.appShell.hiddenDOMWindow.openPreferences(...args);
    483    }
    484  },
    485 };