Source code
Revision control
Copy as Markdown
Other Tools
/* 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
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
ClientID: "resource://gre/modules/ClientID.sys.mjs",
CloseRemoteTab: "resource://gre/modules/FxAccountsCommands.sys.mjs",
FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"CLIENT_ASSOCIATION_PING_ENABLED",
"identity.fxaccounts.telemetry.clientAssociationPing.enabled",
false
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"AlertsService",
"@mozilla.org/alerts-service;1",
"nsIAlertsService"
);
ChromeUtils.defineLazyGetter(
lazy,
"accountsL10n",
() => new Localization(["browser/accounts.ftl", "branding/brand.ftl"], true)
);
/**
* Manages Mozilla Account and Sync related functionality
* needed at startup. It mainly handles various account-related events and notifications.
*
* This module was sliced off of BrowserGlue and designed to centralize
* account-related events/notifications to prevent crowding BrowserGlue
*/
export const AccountsGlue = {
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]),
init() {
let os = Services.obs;
[
"fxaccounts:onverified",
"fxaccounts:device_connected",
"fxaccounts:verify_login",
"fxaccounts:device_disconnected",
"fxaccounts:commands:open-uri",
"fxaccounts:commands:close-uri",
"sync-ui-state:update",
].forEach(topic => os.addObserver(this, topic, true));
},
observe(subject, topic, data) {
switch (topic) {
case "fxaccounts:onverified":
this._onThisDeviceConnected();
break;
case "fxaccounts:device_connected":
this._onDeviceConnected(data);
break;
case "fxaccounts:verify_login":
this._onVerifyLoginNotification(JSON.parse(data));
break;
case "fxaccounts:device_disconnected":
data = JSON.parse(data);
if (data.isLocalDevice) {
this._onDeviceDisconnected();
}
break;
case "fxaccounts:commands:open-uri":
this._onDisplaySyncURIs(subject);
break;
case "fxaccounts:commands:close-uri":
this._onIncomingCloseTabCommand(subject);
break;
case "sync-ui-state:update": {
this._updateFxaBadges(lazy.BrowserWindowTracker.getTopWindow());
if (lazy.CLIENT_ASSOCIATION_PING_ENABLED) {
let fxaState = lazy.UIState.get();
if (fxaState.status == lazy.UIState.STATUS_SIGNED_IN) {
Glean.clientAssociation.uid.set(fxaState.uid);
Glean.clientAssociation.legacyClientId.set(
lazy.ClientID.getCachedClientID()
);
}
}
break;
}
case "browser-glue-test": // used by tests
if (data == "mock-alerts-service") {
// eslint-disable-next-line mozilla/valid-lazy
Object.defineProperty(lazy, "AlertsService", {
value: subject.wrappedJSObject,
});
}
break;
}
},
_onThisDeviceConnected() {
const [title, body] = lazy.accountsL10n.formatValuesSync([
"account-connection-title-2",
"account-connection-connected",
]);
let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
this._openPreferences("sync");
};
lazy.AlertsService.showAlertNotification(
null,
title,
body,
true,
null,
clickCallback
);
},
_openURLInNewWindow(url) {
let urlString = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
urlString.data = url;
return new Promise(resolve => {
let win = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
"_blank",
"chrome,all,dialog=no",
urlString
);
win.addEventListener(
"load",
() => {
resolve(win);
},
{ once: true }
);
});
},
/**
* Called as an observer when Sync's "display URIs" notification is fired.
* We open the received URIs in background tabs.
*
* @param {object} data
* The data passed to the observer notification, which contains
* a wrappedJSObject with the URIs to open.
*/
async _onDisplaySyncURIs(data) {
try {
// The payload is wrapped weirdly because of how Sync does notifications.
const URIs = data.wrappedJSObject.object;
// win can be null, but it's ok, we'll assign it later in openTab()
let win = lazy.BrowserWindowTracker.getTopWindow({ private: false });
const openTab = async URI => {
let tab;
if (!win) {
win = await this._openURLInNewWindow(URI.uri);
let tabs = win.gBrowser.tabs;
tab = tabs[tabs.length - 1];
} else {
tab = win.gBrowser.addWebTab(URI.uri);
}
tab.attention = true;
return tab;
};
const firstTab = await openTab(URIs[0]);
await Promise.all(URIs.slice(1).map(URI => openTab(URI)));
const deviceName = URIs[0].sender && URIs[0].sender.name;
let titleL10nId, body;
if (URIs.length == 1) {
// we have separate strings to handle those cases. (See Also
// unnamedTabsArrivingNotificationNoDevice.body below)
titleL10nId = deviceName
? {
id: "account-single-tab-arriving-from-device-title",
args: { deviceName },
}
: { id: "account-single-tab-arriving-title" };
// Use the page URL as the body. We strip the fragment and query (after
// the `?` and `#` respectively) to reduce size, and also format it the
// same way that the url bar would.
let url = URIs[0].uri.replace(/([?#]).*$/, "$1");
const wasTruncated = url.length < URIs[0].uri.length;
url = lazy.BrowserUIUtils.trimURL(url);
if (wasTruncated) {
body = await lazy.accountsL10n.formatValue(
"account-single-tab-arriving-truncated-url",
{ url }
);
} else {
body = url;
}
} else {
titleL10nId = { id: "account-multiple-tabs-arriving-title" };
const allKnownSender = URIs.every(URI => URI.sender != null);
const allSameDevice =
allKnownSender &&
URIs.every(URI => URI.sender.id == URIs[0].sender.id);
let bodyL10nId;
if (allSameDevice) {
bodyL10nId = deviceName
? "account-multiple-tabs-arriving-from-single-device"
: "account-multiple-tabs-arriving-from-unknown-device";
} else {
bodyL10nId = "account-multiple-tabs-arriving-from-multiple-devices";
}
body = await lazy.accountsL10n.formatValue(bodyL10nId, {
deviceName,
tabCount: URIs.length,
});
}
const title = await lazy.accountsL10n.formatValue(titleL10nId);
const clickCallback = (obsSubject, obsTopic) => {
if (obsTopic == "alertclickcallback") {
win.gBrowser.selectedTab = firstTab;
}
};
// Specify an icon because on Windows no icon is shown at the moment
let imageURL;
if (AppConstants.platform == "win") {
imageURL = "chrome://branding/content/icon64.png";
}
lazy.AlertsService.showAlertNotification(
imageURL,
title,
body,
true,
null,
clickCallback
);
} catch (ex) {
console.error("Error displaying tab(s) received by Sync: ", ex);
}
},
async _onIncomingCloseTabCommand(data) {
// The payload is wrapped weirdly because of how Sync does notifications.
const wrappedObj = data.wrappedJSObject.object;
let { urls } = wrappedObj[0];
let urisToClose = [];
urls.forEach(urlString => {
try {
urisToClose.push(Services.io.newURI(urlString));
} catch (ex) {
// The url was invalid so we ignore
console.error(ex);
}
});
// We want to keep track of the tabs we closed for the notification
// given that there could be duplicates we also closed
let totalClosedTabs = 0;
const windows = lazy.BrowserWindowTracker.orderedWindows;
async function closeTabsInWindows() {
for (const win of windows) {
if (!win.gBrowser) {
continue;
}
try {
const closedInWindow = await win.gBrowser.closeTabsByURI(urisToClose);
totalClosedTabs += closedInWindow;
} catch (ex) {
this.log.error("Error closing tabs in window:", ex);
}
}
}
await closeTabsInWindows();
let clickCallback = async (subject, topic) => {
if (topic == "alertshow") {
// Keep track of the fact that we showed the notification to
// the user at least once
lazy.CloseRemoteTab.hasPendingCloseTabNotification = true;
}
// The notification is either turned off or dismissed by user
if (topic == "alertfinished") {
// Reset the notification pending flag
lazy.CloseRemoteTab.hasPendingCloseTabNotification = false;
}
if (topic != "alertclickcallback") {
return;
}
let win =
lazy.BrowserWindowTracker.getTopWindow({ private: false }) ??
(await lazy.BrowserWindowTracker.promiseOpenWindow());
// We don't want to open a new tab, instead use the handler
// to switch to the existing view
if (win) {
win.FirefoxViewHandler.openTab("recentlyclosed");
}
};
let imageURL;
if (AppConstants.platform == "win") {
imageURL = "chrome://branding/content/icon64.png";
}
// Reset the count only if there are no pending notifications
if (!lazy.CloseRemoteTab.hasPendingCloseTabNotification) {
lazy.CloseRemoteTab.closeTabNotificationCount = 0;
}
lazy.CloseRemoteTab.closeTabNotificationCount += totalClosedTabs;
const [title, body] = await lazy.accountsL10n.formatValues([
{
id: "account-tabs-closed-remotely",
args: { closedCount: lazy.CloseRemoteTab.closeTabNotificationCount },
},
{ id: "account-view-recently-closed-tabs" },
]);
try {
lazy.AlertsService.showAlertNotification(
imageURL,
title,
body,
true,
null,
clickCallback,
"closed-tab-notification"
);
} catch (ex) {
console.error("Error notifying user of closed tab(s) ", ex);
}
},
async _onVerifyLoginNotification({ body, title, url }) {
let tab;
let imageURL;
if (AppConstants.platform == "win") {
imageURL = "chrome://branding/content/icon64.png";
}
let win = lazy.BrowserWindowTracker.getTopWindow({ private: false });
if (!win) {
win = await this._openURLInNewWindow(url);
let tabs = win.gBrowser.tabs;
tab = tabs[tabs.length - 1];
} else {
tab = win.gBrowser.addWebTab(url);
}
tab.attention = true;
let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
win.gBrowser.selectedTab = tab;
};
try {
lazy.AlertsService.showAlertNotification(
imageURL,
title,
body,
true,
null,
clickCallback
);
} catch (ex) {
console.error("Error notifying of a verify login event: ", ex);
}
},
_onDeviceConnected(deviceName) {
const [title, body] = lazy.accountsL10n.formatValuesSync([
{ id: "account-connection-title-2" },
deviceName
? { id: "account-connection-connected-with", args: { deviceName } }
: { id: "account-connection-connected-with-noname" },
]);
let clickCallback = async (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
let url = await lazy.FxAccounts.config.promiseManageDevicesURI(
"device-connected-notification"
);
let win = lazy.BrowserWindowTracker.getTopWindow({ private: false });
if (!win) {
this._openURLInNewWindow(url);
} else {
win.gBrowser.addWebTab(url);
}
};
try {
lazy.AlertsService.showAlertNotification(
null,
title,
body,
true,
null,
clickCallback
);
} catch (ex) {
console.error("Error notifying of a new Sync device: ", ex);
}
},
_onDeviceDisconnected() {
const [title, body] = lazy.accountsL10n.formatValuesSync([
"account-connection-title-2",
"account-connection-disconnected",
]);
let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
this._openPreferences("sync");
};
lazy.AlertsService.showAlertNotification(
null,
title,
body,
true,
null,
clickCallback
);
},
_updateFxaBadges(win) {
let fxaButton = win.document.getElementById("fxa-toolbar-menu-button");
let badge = fxaButton?.querySelector(".toolbarbutton-badge");
let state = lazy.UIState.get();
if (
state.status == lazy.UIState.STATUS_LOGIN_FAILED ||
state.status == lazy.UIState.STATUS_NOT_VERIFIED
) {
// If the fxa toolbar button is in the toolbox, we display the notification
// on the fxa button instead of the app menu.
let navToolbox = win.document.getElementById("navigator-toolbox");
let isFxAButtonShown = navToolbox.contains(fxaButton);
if (isFxAButtonShown) {
state.status == lazy.UIState.STATUS_LOGIN_FAILED
? fxaButton?.setAttribute("badge-status", state.status)
: badge?.classList.add("feature-callout");
} else {
lazy.AppMenuNotifications.showBadgeOnlyNotification(
"fxa-needs-authentication"
);
}
} else {
fxaButton?.removeAttribute("badge-status");
badge?.classList.remove("feature-callout");
lazy.AppMenuNotifications.removeNotification("fxa-needs-authentication");
}
},
// Open preferences even if there are no open windows.
_openPreferences(...args) {
let chromeWindow = lazy.BrowserWindowTracker.getTopWindow();
if (chromeWindow) {
chromeWindow.openPreferences(...args);
return;
}
if (AppConstants.platform == "macosx") {
Services.appShell.hiddenDOMWindow.openPreferences(...args);
}
},
};