--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -318,16 +318,23 @@ var gFxAccounts = {
replaceQueryString: true
});
},
openSignInAgainPage(entryPoint) {
this.openAccountsPage("reauth", { entrypoint: entryPoint });
},
+ async openDevicesManagementPage(entryPoint) {
+ let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
+ switchToTabHavingURI(url, true, {
+ replaceQueryString: true
+ });
+ },
+
sendTabToDevice(url, clientId, title) {
Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
},
populateSendTabToDevicesMenu(devicesPopup, url, title) {
// remove existing menu items
while (devicesPopup.hasChildNodes()) {
devicesPopup.firstChild.remove();
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -473,16 +473,20 @@
id="syncedTabsBookmarkSelected"/>
<menuitem label="&syncedTabs.context.copy.label;"
accesskey="&syncedTabs.context.copy.accesskey;"
id="syncedTabsCopySelected"/>
<menuseparator/>
<menuitem label="&syncedTabs.context.openAllInTabs.label;"
accesskey="&syncedTabs.context.openAllInTabs.accesskey;"
id="syncedTabsOpenAllInTabs"/>
+ <menuitem label="&syncedTabs.context.managedevices.label;"
+ accesskey="&syncedTabs.context.managedevices.accesskey;"
+ id="syncedTabsManageDevices"
+ oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-sidebar');"/>
<menuitem label="&syncSyncNowItem.label;"
accesskey="&syncSyncNowItem.accesskey;"
id="syncedTabsRefresh"/>
</menupopup>
<menupopup id="SyncedTabsSidebarTabsFilterContext"
class="textbox-contextmenu">
<menuitem label="&undoCmd.label;"
accesskey="&undoCmd.accesskey;"
--- a/browser/base/content/test/general/browser_aboutAccounts.js
+++ b/browser/base/content/test/general/browser_aboutAccounts.js
@@ -195,23 +195,23 @@ var gTests = [
},
{
desc: "Test action=reauth",
*teardown() {
gBrowser.removeCurrentTab();
yield signOut();
},
*run() {
- const expected_url = "https://example.com/?is_force_auth";
+ const expected_url = "https://example.com/force_auth";
setPref("identity.fxaccounts.remote.force_auth.uri", expected_url);
yield setSignedInUser();
let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=reauth");
// The current user will be appended to the url
- let expected = expected_url + "&email=foo%40example.com";
+ let expected = expected_url + "?uid=1234%40lcip.org&email=foo%40example.com";
is(url, expected, "action=reauth got the expected URL");
},
},
{
desc: "Test with migrateToDevEdition enabled (success)",
*teardown() {
gBrowser.removeCurrentTab();
yield signOut();
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -109,16 +109,20 @@
<!-- this widget has 3 boxes in the body, but only 1 is ever visible -->
<!-- When Sync is ready to sync -->
<vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state">
<vbox id="PanelUI-remotetabs-buttons">
<toolbarbutton id="PanelUI-remotetabs-view-sidebar"
class="subviewbutton"
observes="viewTabsSidebar"
label="&appMenuRemoteTabs.sidebar.label;"/>
+ <toolbarbutton id="PanelUI-remotetabs-view-managedevices"
+ class="subviewbutton"
+ label="&appMenuRemoteTabs.managedevices.label;"
+ oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-menupanel');"/>
<toolbarbutton id="PanelUI-remotetabs-syncnow"
observes="sync-status"
class="subviewbutton"
oncommand="gSyncUI.doSync();"
closemenu="none"/>
<menuseparator id="PanelUI-remotetabs-separator"/>
</vbox>
<deck id="PanelUI-remotetabs-deck">
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -143,16 +143,18 @@ function BrowserGlue() {
XPCOMUtils.defineLazyGetter(this, "_sanitizer",
function() {
let sanitizerScope = {};
Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", sanitizerScope);
return sanitizerScope.Sanitizer;
});
+ XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
+
this._init();
}
/*
* OS X has the concept of zero-window sessions and therefore ignores the
* browser-lastwindow-close-* topics.
*/
const OBSERVE_LASTWINDOW_CLOSE_TOPICS = AppConstants.platform != "macosx";
@@ -300,16 +302,20 @@ BrowserGlue.prototype = {
this._distributionCustomizer.applyCustomizations();
// To apply distribution bookmarks use "places-init-complete".
} else if (data == "force-places-init") {
this._initPlaces(false);
} else if (data == "smart-bookmarks-init") {
this.ensurePlacesDefaultQueriesInitialized().then(() => {
Services.obs.notifyObservers(null, "test-smart-bookmarks-done", null);
});
+ } else if (data == "mock-fxaccounts") {
+ Object.defineProperty(this, "fxAccounts", {
+ value: subject.wrappedJSObject
+ });
}
break;
case "initial-migration-will-import-default-bookmarks":
this._migrationImportsDefaultBookmarks = true;
break;
case "initial-migration-did-import-default-bookmarks":
this._initPlaces(true);
break;
@@ -2297,31 +2303,31 @@ BrowserGlue.prototype = {
_onDeviceConnected(deviceName) {
let accountsBundle = Services.strings.createBundle(
"chrome://browser/locale/accounts.properties"
);
let title = accountsBundle.GetStringFromName("deviceConnectedTitle");
let body = accountsBundle.formatStringFromName("deviceConnectedBody" +
(deviceName ? "" : ".noDeviceName"),
[deviceName], 1);
- let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.devices.uri");
- function clickCallback(subject, topic, data) {
+ let clickCallback = async (subject, topic, data) => {
if (topic != "alertclickcallback")
return;
+ let url = await this.fxAccounts.promiseAccountsManageDevicesURI("device-connected-notification");
let win = RecentWindow.getMostRecentBrowserWindow({private: false});
if (!win) {
Services.appShell.hiddenDOMWindow.open(url);
} else {
win.gBrowser.addTab(url);
}
- }
+ };
try {
- AlertsService.showAlertNotification(null, title, body, true, url, clickCallback);
+ AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
} catch (ex) {
Cu.reportError("Error notifying of a new Sync device: " + ex);
}
},
_onDeviceDisconnected() {
let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
let title = bundle.GetStringFromName("deviceDisconnectedNotification.title");
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -519,24 +519,27 @@ TabListView.prototype = {
let item = this.container.querySelector(".item.selected");
let showTabOptions = this._isTab(item);
let el = menu.firstChild;
while (el) {
let show = false;
if (showTabOptions) {
- if (el.getAttribute("id") != "syncedTabsOpenAllInTabs") {
+ if (el.getAttribute("id") != "syncedTabsOpenAllInTabs" &&
+ el.getAttribute("id") != "syncedTabsManageDevices") {
show = true;
}
} else if (el.getAttribute("id") == "syncedTabsOpenAllInTabs") {
const tabs = item.querySelectorAll(".item-tabs-list > .item.tab");
show = tabs.length > 0;
} else if (el.getAttribute("id") == "syncedTabsRefresh") {
show = true;
+ } else if (el.getAttribute("id") == "syncedTabsManageDevices") {
+ show = true;
}
el.hidden = !show;
el = el.nextSibling;
}
},
/**
--- a/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
+++ b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
@@ -305,34 +305,36 @@ add_task(function* testSyncedTabsSidebar
["menuitem#syncedTabsOpenSelectedInTab", { hidden: false }],
["menuitem#syncedTabsOpenSelectedInWindow", { hidden: false }],
["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: false }],
["menuseparator", { hidden: false }],
["menuitem#syncedTabsBookmarkSelected", { hidden: false }],
["menuitem#syncedTabsCopySelected", { hidden: false }],
["menuseparator", { hidden: false }],
["menuitem#syncedTabsOpenAllInTabs", { hidden: true }],
+ ["menuitem#syncedTabsManageDevices", { hidden: true }],
["menuitem#syncedTabsRefresh", { hidden: false }],
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarContext",
"#tab-7cqCr77ptzX3-0",
tabMenuItems);
- info("Right-clicking a client should show the Open All in Tabs action");
+ info("Right-clicking a client should show the Open All in Tabs and Manage devices actions");
let sidebarMenuItems = [
["menuitem#syncedTabsOpenSelected", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInTab", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInWindow", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: true }],
["menuseparator", { hidden: true }],
["menuitem#syncedTabsBookmarkSelected", { hidden: true }],
["menuitem#syncedTabsCopySelected", { hidden: true }],
["menuseparator", { hidden: true }],
["menuitem#syncedTabsOpenAllInTabs", { hidden: false }],
+ ["menuitem#syncedTabsManageDevices", { hidden: false }],
["menuitem#syncedTabsRefresh", { hidden: false }],
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarContext",
"#item-7cqCr77ptzX3",
sidebarMenuItems);
info("Right-clicking a client without any tabs should not show the Open All in Tabs action");
@@ -341,16 +343,17 @@ add_task(function* testSyncedTabsSidebar
["menuitem#syncedTabsOpenSelectedInTab", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInWindow", { hidden: true }],
["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: true }],
["menuseparator", { hidden: true }],
["menuitem#syncedTabsBookmarkSelected", { hidden: true }],
["menuitem#syncedTabsCopySelected", { hidden: true }],
["menuseparator", { hidden: true }],
["menuitem#syncedTabsOpenAllInTabs", { hidden: true }],
+ ["menuitem#syncedTabsManageDevices", { hidden: false }],
["menuitem#syncedTabsRefresh", { hidden: false }],
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarContext",
"#item-OL3EJCsdb2JD",
menuItems);
});
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -372,16 +372,17 @@ These should match what Safari and other
<!-- LOCALIZATION NOTE (appMenuRemoteTabs.noclients.label): This is shown
when Sync is configured but this appears to be the only device attached to
the account. We also show links to download Firefox for android/ios. -->
<!ENTITY appMenuRemoteTabs.noclients.title "No synced tabs… yet!">
<!ENTITY appMenuRemoteTabs.noclients.subtitle "Want to see your tabs from other devices here?">
<!ENTITY appMenuRemoteTabs.openprefs.label "Sync Preferences">
<!ENTITY appMenuRemoteTabs.notsignedin.label "Sign in to view a list of tabs from your other devices.">
<!ENTITY appMenuRemoteTabs.signin.label "Sign in to Sync">
+<!ENTITY appMenuRemoteTabs.managedevices.label "Manage Devices…">
<!ENTITY appMenuRemoteTabs.sidebar.label "View Synced Tabs Sidebar">
<!ENTITY customizeMenu.addToToolbar.label "Add to Toolbar">
<!ENTITY customizeMenu.addToToolbar.accesskey "A">
<!ENTITY customizeMenu.addToPanel.label "Add to Menu">
<!ENTITY customizeMenu.addToPanel.accesskey "M">
<!ENTITY customizeMenu.moveToToolbar.label "Move to Toolbar">
<!ENTITY customizeMenu.moveToToolbar.accesskey "o">
@@ -780,16 +781,18 @@ you can use these alternative items. Oth
<!ENTITY syncedTabs.context.openInNewPrivateWindow.accesskey "P">
<!ENTITY syncedTabs.context.bookmarkSingleTab.label "Bookmark This Tab…">
<!ENTITY syncedTabs.context.bookmarkSingleTab.accesskey "B">
<!ENTITY syncedTabs.context.copy.label "Copy">
<!ENTITY syncedTabs.context.copy.accesskey "C">
<!ENTITY syncedTabs.context.openAllInTabs.label "Open All in Tabs">
<!ENTITY syncedTabs.context.openAllInTabs.accesskey "O">
+<!ENTITY syncedTabs.context.managedevices.label "Manage Devices…">
+<!ENTITY syncedTabs.context.managedevices.accesskey "D">
<!ENTITY syncBrand.shortName.label "Sync">
<!ENTITY syncSignIn.label "Sign In To &syncBrand.shortName.label;…">
<!ENTITY syncSignIn.accesskey "Y">
<!ENTITY syncSyncNowItem.label "Sync Now">
<!ENTITY syncSyncNowItem.accesskey "S">
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -2,16 +2,18 @@
* 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";
this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.importGlobalProperties(["URL"]);
+
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
@@ -54,16 +56,17 @@ var publicProperties = [
"invalidateCertificate",
"loadAndPoll",
"localtimeOffsetMsec",
"notifyDevices",
"now",
"promiseAccountsChangeProfileURI",
"promiseAccountsForceSigninURI",
"promiseAccountsManageURI",
+ "promiseAccountsManageDevicesURI",
"promiseAccountsSignUpURI",
"promiseAccountsSignInURI",
"removeCachedOAuthToken",
"requiresHttps",
"resendVerificationEmail",
"resetCredentials",
"sessionStatus",
"setProfileCache",
@@ -1258,90 +1261,76 @@ FxAccountsInternal.prototype = {
promiseAccountsSignUpURI() {
return FxAccountsConfig.promiseAccountsSignUpURI();
},
promiseAccountsSignInURI() {
return FxAccountsConfig.promiseAccountsSignInURI();
},
- // Returns a promise that resolves with the URL to use to force a re-signin
- // of the current account.
- promiseAccountsForceSigninURI: Task.async(function *() {
- yield FxAccountsConfig.ensureConfigured();
- let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri");
- if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+ /**
+ * Pull an URL defined in the user preferences, add the current UID and email
+ * to the query string, add entrypoint and extra params to the query string if
+ * requested.
+ * @param {string} prefName The preference name from where to pull the URL to format.
+ * @param {string} [entrypoint] "entrypoint" searchParam value.
+ * @param {Object.<string, string>} [extraParams] Additionnal searchParam key and values.
+ * @returns {Promise.<string>} A promise that resolves to the formatted URL
+ */
+ async _formatPrefURL(prefName, entrypoint, extraParams) {
+ let url = new URL(Services.urlFormatter.formatURLPref(prefName));
+ if (this.requiresHttps() && url.protocol != "https:") {
throw new Error("Firefox Accounts server must use HTTPS");
}
- let currentState = this.currentAccountState;
- // but we need to append the email address onto a query string.
- return this.getSignedInUser().then(accountData => {
- if (!accountData) {
- return null;
+ let accountData = await this.getSignedInUser();
+ if (!accountData) {
+ return Promise.resolve(null);
+ }
+ url.searchParams.append("uid", accountData.uid);
+ url.searchParams.append("email", accountData.email);
+ if (entrypoint) {
+ url.searchParams.append("entrypoint", entrypoint);
+ }
+ if (extraParams) {
+ for (let [k, v] of Object.entries(extraParams)) {
+ url.searchParams.append(k, v);
}
- let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
- newQueryPortion += "email=" + encodeURIComponent(accountData.email);
- return url + newQueryPortion;
- }).then(result => currentState.resolve(result));
- }),
+ }
+ return this.currentAccountState.resolve(url.href);
+ },
+
+ // Returns a promise that resolves with the URL to use to force a re-signin
+ // of the current account.
+ async promiseAccountsForceSigninURI() {
+ await FxAccountsConfig.ensureConfigured();
+ return this._formatPrefURL("identity.fxaccounts.remote.force_auth.uri");
+ },
// Returns a promise that resolves with the URL to use to change
// the current account's profile image.
// if settingToEdit is set, the profile page should hightlight that setting
// for the user to edit.
- promiseAccountsChangeProfileURI(entrypoint, settingToEdit = null) {
- let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
-
+ async promiseAccountsChangeProfileURI(entrypoint, settingToEdit = null) {
+ let extraParams;
if (settingToEdit) {
- url += (url.indexOf("?") == -1 ? "?" : "&") +
- "setting=" + encodeURIComponent(settingToEdit);
- }
-
- if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
- throw new Error("Firefox Accounts server must use HTTPS");
+ extraParams = { setting: settingToEdit };
}
- let currentState = this.currentAccountState;
- // but we need to append the email address onto a query string.
- return this.getSignedInUser().then(accountData => {
- if (!accountData) {
- return null;
- }
- let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
- newQueryPortion += "email=" + encodeURIComponent(accountData.email);
- newQueryPortion += "&uid=" + encodeURIComponent(accountData.uid);
- if (entrypoint) {
- newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint);
- }
- return url + newQueryPortion;
- }).then(result => currentState.resolve(result));
+ return this._formatPrefURL("identity.fxaccounts.settings.uri", entrypoint, extraParams);
},
// Returns a promise that resolves with the URL to use to manage the current
// user's FxA acct.
- promiseAccountsManageURI(entrypoint) {
- let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
- if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
- throw new Error("Firefox Accounts server must use HTTPS");
- }
- let currentState = this.currentAccountState;
- // but we need to append the uid and email address onto a query string
- // (if the server has no matching uid it will offer to sign in with the
- // email address)
- return this.getSignedInUser().then(accountData => {
- if (!accountData) {
- return null;
- }
- let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
- newQueryPortion += "uid=" + encodeURIComponent(accountData.uid) +
- "&email=" + encodeURIComponent(accountData.email);
- if (entrypoint) {
- newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint);
- }
- return url + newQueryPortion;
- }).then(result => currentState.resolve(result));
+ async promiseAccountsManageURI(entrypoint) {
+ return this._formatPrefURL("identity.fxaccounts.settings.uri", entrypoint);
+ },
+
+ // Returns a promise that resolves with the URL to use to manage the devices in
+ // the current user's FxA acct.
+ async promiseAccountsManageDevicesURI(entrypoint) {
+ return this._formatPrefURL("identity.fxaccounts.settings.devices.uri", entrypoint);
},
/**
* Get an OAuth token for the user
*
* @param options
* {
* scope: (string/array) the oauth scope(s) being requested. As a
--- a/services/fxaccounts/tests/browser/browser_device_connected.js
+++ b/services/fxaccounts/tests/browser/browser_device_connected.js
@@ -1,23 +1,31 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
const { MockRegistrar } =
Cu.import("resource://testing-common/MockRegistrar.jsm", {});
-
-let accountsBundle = Services.strings.createBundle(
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver);
+const accountsBundle = Services.strings.createBundle(
"chrome://browser/locale/accounts.properties"
);
+const DEVICES_URL = "http://localhost/devices";
let expectedBody;
add_task(async function setup() {
+ let fxAccounts = {
+ promiseAccountsManageDevicesURI() {
+ return Promise.resolve(DEVICES_URL);
+ }
+ };
+ gBrowserGlue.observe({wrappedJSObject: fxAccounts}, "browser-glue-test", "mock-fxaccounts");
const alertsService = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService, Ci.nsISupports]),
showAlertNotification: (image, title, text, clickable, cookie, clickCallback) => {
// We can't simulate a click on the alert popup,
// so instead we call the click listener ourselves directly
clickCallback.observe(null, "alertclickcallback", null);
Assert.equal(text, expectedBody);
}
@@ -28,28 +36,24 @@ add_task(async function setup() {
});
});
async function testDeviceConnected(deviceName) {
info("testDeviceConnected with deviceName=" + deviceName);
gBrowser.selectedBrowser.loadURI("about:robots");
await waitForDocLoadComplete();
- Preferences.set("identity.fxaccounts.settings.devices.uri", "http://localhost/devices");
-
let waitForTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
Services.obs.notifyObservers(null, "fxaccounts:device_connected", deviceName);
let tab = await waitForTabPromise;
Assert.ok("Tab successfully opened");
- let expectedURI = Preferences.get("identity.fxaccounts.settings.devices.uri",
- "prefundefined");
- Assert.equal(tab.linkedBrowser.currentURI.spec, expectedURI);
+ Assert.equal(tab.linkedBrowser.currentURI.spec, DEVICES_URL);
await BrowserTestUtils.removeTab(tab);
}
add_task(async function() {
expectedBody = accountsBundle.formatStringFromName("deviceConnectedBody", ["My phone"], 1);
await testDeviceConnected("My phone");
});