492 lines
15 KiB
JavaScript
492 lines
15 KiB
JavaScript
/* 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/. */
|
|
|
|
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",
|
|
UIState: "resource://services-sync/UIState.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) {
|
|
// Due to bug 1305895, tabs from iOS may not have device information, so
|
|
// 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);
|
|
}
|
|
},
|
|
};
|