author | Edouard Oger <eoger@fastmail.com> |
Fri, 16 Nov 2018 03:03:13 +0000 | |
changeset 446787 | 68841d4d23dafb234273bbe4e6dbf06056c4ffe9 |
parent 446786 | f3cb436aa13e76996b3b0349bd604f5183f7ee1c |
child 446788 | 599db31d59659e1e1bbf37eea6d12194811aa384 |
push id | 35052 |
push user | apavel@mozilla.com |
push date | Sat, 17 Nov 2018 11:25:40 +0000 |
treeherder | mozilla-central@efc1da42132b [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | markh |
bugs | 1507294 |
milestone | 65.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/browser-sync.js +++ b/browser/base/content/browser-sync.js @@ -55,19 +55,20 @@ var gSync = { return UIState.get().status == UIState.STATUS_SIGNED_IN && (!this.syncReady || Weave.Service.clientsEngine.isFirstSync); }, get isSignedIn() { return UIState.get().status == UIState.STATUS_SIGNED_IN; }, - get remoteClients() { - return Weave.Service.clientsEngine.remoteClients - .sort((a, b) => a.name.localeCompare(b.name)); + get sendTabTargets() { + return Weave.Service.clientsEngine.fxaDevices + .sort((a, b) => a.name.localeCompare(b.name)) + .filter(d => !d.isCurrentDevice && (fxAccounts.commands.sendTab.isDeviceCompatible(d) || d.clientRecord)); }, get offline() { return Weave.Service.scheduler.offline; }, _generateNodeGetters() { for (let k of ["Status", "Avatar", "Label", "Container"]) { @@ -311,64 +312,54 @@ var gSync = { }, openSendToDevicePromo() { let url = this.PRODUCT_INFO_BASE_URL; url += "send-tabs/?utm_source=" + Services.appinfo.name.toLowerCase(); switchToTabHavingURI(url, true, { replaceQueryString: true }); }, - async sendTabToDevice(url, clients, title) { - let devices; - try { - devices = await fxAccounts.getDeviceList(); - } catch (e) { - console.error("Could not get the FxA device list", e); - devices = []; // We can still run in degraded mode. - } + async sendTabToDevice(url, targets, title) { const fxaCommandsDevices = []; const oldSendTabClients = []; - for (const client of clients) { - const device = devices.find(d => d.id == client.fxaDeviceId); - if (!device) { - console.error(`Could not find associated FxA device for ${client.name}`); - continue; - } else if ((await fxAccounts.commands.sendTab.isDeviceCompatible(device))) { - fxaCommandsDevices.push(device); + for (const target of targets) { + if (fxAccounts.commands.sendTab.isDeviceCompatible(target)) { + fxaCommandsDevices.push(target); + } else if (target.clientRecord) { + oldSendTabClients.push(target.clientRecord); } else { - oldSendTabClients.push(client); + console.error(`Target ${target.id} unsuitable for send tab.`); } } if (fxaCommandsDevices.length) { console.log(`Sending a tab to ${fxaCommandsDevices.map(d => d.name).join(", ")} using FxA commands.`); const report = await fxAccounts.commands.sendTab.send(fxaCommandsDevices, {url, title}); for (let {device, error} of report.failed) { console.error(`Failed to send a tab with FxA commands for ${device.name}. Falling back on the Sync back-end`, error); - const client = clients.find(c => c.fxaDeviceId == device.id); - if (!client) { + if (!device.clientRecord) { console.error(`Could not find associated Sync device for ${device.name}`); continue; } - oldSendTabClients.push(client); + oldSendTabClients.push(device.clientRecord); } } for (let client of oldSendTabClients) { try { console.log(`Sending a tab to ${client.name} using Sync.`); await Weave.Service.clientsEngine.sendURIToClientForDisplay(url, client.id, title); } catch (e) { console.error("Could not send tab to device.", e); } } }, populateSendTabToDevicesMenu(devicesPopup, url, title, multiselected, createDeviceNodeFn) { if (!createDeviceNodeFn) { - createDeviceNodeFn = (clientId, name, clientType, lastModified) => { + createDeviceNodeFn = (targetId, name, targetType, lastModified) => { let eltName = name ? "menuitem" : "menuseparator"; return document.createXULElement(eltName); }; } // remove existing menu items for (let i = devicesPopup.children.length - 1; i >= 0; --i) { let child = devicesPopup.children[i]; @@ -380,17 +371,17 @@ var gSync = { if (gSync.syncConfiguredAndLoading) { // We can only be in this case in the page action menu. return; } const fragment = document.createDocumentFragment(); const state = UIState.get(); - if (state.status == UIState.STATUS_SIGNED_IN && this.remoteClients.length > 0) { + if (state.status == UIState.STATUS_SIGNED_IN && this.sendTabTargets.length > 0) { this._appendSendTabDeviceList(fragment, createDeviceNodeFn, url, title, multiselected); } else if (state.status == UIState.STATUS_SIGNED_IN) { this._appendSendTabSingleDevice(fragment, createDeviceNodeFn); } else if (state.status == UIState.STATUS_NOT_VERIFIED || state.status == UIState.STATUS_LOGIN_FAILED) { this._appendSendTabVerify(fragment, createDeviceNodeFn); } else /* status is STATUS_NOT_CONFIGURED */ { this._appendSendTabUnconfigured(fragment, createDeviceNodeFn); @@ -398,56 +389,64 @@ var gSync = { devicesPopup.appendChild(fragment); }, // TODO: once our transition from the old-send tab world is complete, // this list should be built using the FxA device list instead of the client // collection. _appendSendTabDeviceList(fragment, createDeviceNodeFn, url, title, multiselected) { + const targets = this.sendTabTargets; + let tabsToSend = multiselected ? gBrowser.selectedTabs.map(t => { return { url: t.linkedBrowser.currentURI.spec, title: t.linkedBrowser.contentTitle, }; }) : [{url, title}]; const onSendAllCommand = (event) => { for (let t of tabsToSend) { - this.sendTabToDevice(t.url, this.remoteClients, t.title); + this.sendTabToDevice(t.url, targets, t.title); } }; const onTargetDeviceCommand = (event) => { - const clientId = event.target.getAttribute("clientId"); - const client = this.remoteClients.find(c => c.id == clientId); + const targetId = event.target.getAttribute("clientId"); + const target = targets.find(t => t.id == targetId); for (let t of tabsToSend) { - this.sendTabToDevice(t.url, [client], t.title); + this.sendTabToDevice(t.url, [target], t.title); } }; - function addTargetDevice(clientId, name, clientType, lastModified) { - const targetDevice = createDeviceNodeFn(clientId, name, clientType, lastModified); - targetDevice.addEventListener("command", clientId ? onTargetDeviceCommand : + function addTargetDevice(targetId, name, targetType, lastModified) { + const targetDevice = createDeviceNodeFn(targetId, name, targetType, lastModified); + targetDevice.addEventListener("command", targetId ? onTargetDeviceCommand : onSendAllCommand, true); targetDevice.classList.add("sync-menuitem", "sendtab-target"); - targetDevice.setAttribute("clientId", clientId); - targetDevice.setAttribute("clientType", clientType); + targetDevice.setAttribute("clientId", targetId); + targetDevice.setAttribute("clientType", targetType); targetDevice.setAttribute("label", name); fragment.appendChild(targetDevice); } - const clients = this.remoteClients; - for (let client of clients) { - const type = Weave.Service.clientsEngine.getClientType(client.id); - addTargetDevice(client.id, client.name, type, new Date(client.serverLastModified * 1000)); + for (let target of targets) { + let type, lastModified; + if (target.clientRecord) { + type = Weave.Service.clientsEngine.getClientType(target.clientRecord.id); + lastModified = new Date(target.clientRecord.serverLastModified * 1000); + } else { + type = target.type === "desktop" ? "desktop" : "phone"; // Normalizing the FxA types just in case. + lastModified = null; + } + addTargetDevice(target.id, target.name, type, lastModified); } // "Send to All Devices" menu item - if (clients.length > 1) { + if (targets.length > 1) { const separator = createDeviceNodeFn(); separator.classList.add("sync-menuitem"); fragment.appendChild(separator); const allDevicesLabel = this.fxaStrings.GetStringFromName("sendToAllDevices.menuitem"); addTargetDevice("", allDevicesLabel, ""); } },
--- a/browser/base/content/test/sync/browser_contextmenu_sendpage.js +++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js @@ -1,25 +1,25 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ]; +const targetsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ]; add_task(async function setup() { await promiseSyncReady(); // gSync.init() is called in a requestIdleCallback. Force its initialization. gSync.init(); sinon.stub(Weave.Service.clientsEngine, "getClientType").returns("desktop"); await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); }); add_task(async function test_page_contextmenu() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: remoteClientsFixture, + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture, state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); await openContentContextMenu("#moztext", "context-sendpagetodevice"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled"); checkPopup([ { label: "Foo" }, { label: "Bar" }, @@ -27,17 +27,17 @@ add_task(async function test_page_contex { label: "Send to All Devices" }, ]); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_link_contextmenu() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: remoteClientsFixture, + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture, state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); let expectation = sandbox.mock(gSync) .expects("sendTabToDevice") .once() .withExactArgs("https://www.example.org/", [{id: 1, name: "Foo"}], "Click on me!!"); // Add a link to the page await ContentTask.spawn(gBrowser.selectedBrowser, null, () => { @@ -54,17 +54,17 @@ add_task(async function test_link_contex document.getElementById("context-sendlinktodevice-popup").querySelector("menuitem").click(); await hideContentContextMenu(); expectation.verify(); sandbox.restore(); }); add_task(async function test_page_contextmenu_no_remote_clients() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: [], + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: [], state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); await openContentContextMenu("#moztext", "context-sendpagetodevice"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled"); checkPopup([ { label: "No Devices Connected", disabled: true }, "----", @@ -72,88 +72,88 @@ add_task(async function test_page_contex { label: "Learn About Sending Tabs..." }, ]); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_page_contextmenu_one_remote_client() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: [{ id: 1, name: "Foo"}], + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: [{ id: 1, name: "Foo"}], state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); await openContentContextMenu("#moztext", "context-sendpagetodevice"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled"); checkPopup([ { label: "Foo" }, ]); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_page_contextmenu_not_sendable() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: remoteClientsFixture, + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture, state: UIState.STATUS_SIGNED_IN, isSendableURI: false }); await openContentContextMenu("#moztext"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, true, "Send tab to device is disabled"); checkPopup(); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_page_contextmenu_not_synced_yet() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: false, remoteClients: [], + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: false, targets: [], state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); await openContentContextMenu("#moztext"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, true, "Send tab to device is disabled"); checkPopup(); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_page_contextmenu_sync_not_ready_configured() { - const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, remoteClients: null, + const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null, state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); await openContentContextMenu("#moztext"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, true, "Send tab to device is disabled"); checkPopup(); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_page_contextmenu_sync_not_ready_other_state() { - const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, remoteClients: null, + const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null, state: UIState.STATUS_NOT_VERIFIED, isSendableURI: true }); await openContentContextMenu("#moztext", "context-sendpagetodevice"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled"); checkPopup([ { label: "Account Not Verified", disabled: true }, "----", { label: "Verify Your Account..." }, ]); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_page_contextmenu_unconfigured() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: null, + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: null, state: UIState.STATUS_NOT_CONFIGURED, isSendableURI: true }); await openContentContextMenu("#moztext", "context-sendpagetodevice"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled"); checkPopup([ { label: "Not Connected to Sync", disabled: true }, "----", @@ -162,17 +162,17 @@ add_task(async function test_page_contex ]); await hideContentContextMenu(); sandbox.restore(); }); add_task(async function test_page_contextmenu_not_verified() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: null, + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: null, state: UIState.STATUS_NOT_VERIFIED, isSendableURI: true }); await openContentContextMenu("#moztext", "context-sendpagetodevice"); is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled"); checkPopup([ { label: "Account Not Verified", disabled: true }, "----",
--- a/browser/base/content/test/sync/browser_contextmenu_sendtab.js +++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js @@ -2,17 +2,17 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/"; Services.scriptloader.loadSubScript(chrome_base + "head.js", this); /* import-globals-from ../general/head.js */ -const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ]; +const targetsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ]; let [testTab] = gBrowser.visibleTabs; function updateTabContextMenu(tab) { let menu = document.getElementById("tabContextMenu"); if (!tab) tab = gBrowser.selectedTab; var evt = new Event(""); @@ -30,17 +30,17 @@ add_task(async function setup() { await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); registerCleanupFunction(() => { gBrowser.removeCurrentTab(); }); is(gBrowser.visibleTabs.length, 2, "there are two visible tabs"); }); add_task(async function test_tab_contextmenu() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: remoteClientsFixture, + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture, state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); let expectation = sandbox.mock(gSync) .expects("sendTabToDevice") .once() .withExactArgs("about:mozilla", [{id: 1, name: "Foo"}], "The Book of Mozilla, 11:14"); updateTabContextMenu(testTab); await openTabContextMenu("context_sendTabToDevice"); @@ -50,61 +50,61 @@ add_task(async function test_tab_context document.getElementById("context_sendTabToDevicePopupMenu").querySelector("menuitem").click(); await hideTabContextMenu(); expectation.verify(); sandbox.restore(); }); add_task(async function test_tab_contextmenu_unconfigured() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: remoteClientsFixture, + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture, state: UIState.STATUS_NOT_CONFIGURED, isSendableURI: true }); updateTabContextMenu(testTab); is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled"); sandbox.restore(); }); add_task(async function test_tab_contextmenu_not_sendable() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: [{ id: 1, name: "Foo"}], + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: [{ id: 1, name: "Foo"}], state: UIState.STATUS_SIGNED_IN, isSendableURI: false }); updateTabContextMenu(testTab); is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context_sendTabToDevice").disabled, true, "Send tab to device is disabled"); sandbox.restore(); }); add_task(async function test_tab_contextmenu_not_synced_yet() { - const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: false, remoteClients: [], + const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: false, targets: [], state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); updateTabContextMenu(testTab); is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context_sendTabToDevice").disabled, true, "Send tab to device is disabled"); sandbox.restore(); }); add_task(async function test_tab_contextmenu_sync_not_ready_configured() { - const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, remoteClients: null, + const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null, state: UIState.STATUS_SIGNED_IN, isSendableURI: true }); updateTabContextMenu(testTab); is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context_sendTabToDevice").disabled, true, "Send tab to device is disabled"); sandbox.restore(); }); add_task(async function test_tab_contextmenu_sync_not_ready_other_state() { - const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, remoteClients: null, + const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null, state: UIState.STATUS_NOT_VERIFIED, isSendableURI: true }); updateTabContextMenu(testTab); is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown"); is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled"); sandbox.restore(); });
--- a/browser/base/content/test/sync/head.js +++ b/browser/base/content/test/sync/head.js @@ -9,17 +9,17 @@ registerCleanupFunction(function() { function promiseSyncReady() { let service = Cc["@mozilla.org/weave/service;1"] .getService(Ci.nsISupports) .wrappedJSObject; return service.whenLoaded(); } -function setupSendTabMocks({ syncReady, clientsSynced, remoteClients, state, isSendableURI }) { +function setupSendTabMocks({ syncReady, clientsSynced, targets, state, isSendableURI }) { const sandbox = sinon.sandbox.create(); sandbox.stub(gSync, "syncReady").get(() => syncReady); sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => !clientsSynced); - sandbox.stub(gSync, "remoteClients").get(() => remoteClients); + sandbox.stub(gSync, "sendTabTargets").get(() => targets); sandbox.stub(UIState, "get").returns({ status: state }); sandbox.stub(gSync, "isSendableURI").returns(isSendableURI); return sandbox; }
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js +++ b/browser/base/content/test/urlbar/browser_page_action_menu.js @@ -3,20 +3,21 @@ /* global sinon, UIState */ Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js"); registerCleanupFunction(function() { delete window.sinon; }); const lastModifiedFixture = 1507655615.87; // Approx Oct 10th 2017 -const mockRemoteClients = [ - { id: "0", name: "foo", type: "mobile", serverLastModified: lastModifiedFixture }, - { id: "1", name: "bar", type: "desktop", serverLastModified: lastModifiedFixture }, - { id: "2", name: "baz", type: "mobile", serverLastModified: lastModifiedFixture }, +const mockTargets = [ + { id: "0", name: "foo", type: "phone", clientRecord: {id: "cli0", serverLastModified: lastModifiedFixture, type: "phone"} }, + { id: "1", name: "bar", type: "desktop", clientRecord: {id: "cli1", serverLastModified: lastModifiedFixture, type: "desktop"} }, + { id: "2", name: "baz", type: "phone", clientRecord: {id: "cli2", serverLastModified: lastModifiedFixture, type: "phone"} }, + { id: "3", name: "no client record device", type: "phone" }, ]; add_task(async function bookmark() { // Open a unique page. let url = "http://example.com/browser_page_action_menu"; await BrowserTestUtils.withNewTab(url, async () => { // Open the panel. await promisePageActionPanelOpen(); @@ -263,18 +264,18 @@ add_task(async function sendToDevice_syn const syncReady = sandbox.stub(gSync, "syncReady").get(() => false); const lastSync = sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => true); sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN }); sandbox.stub(gSync, "isSendableURI").returns(true); sandbox.stub(Weave.Service, "sync").callsFake(() => { syncReady.get(() => true); lastSync.get(() => Date.now()); - sandbox.stub(gSync, "remoteClients").get(() => mockRemoteClients); - sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockRemoteClients.find(c => c.id == id).type); + sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets); + sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type); }); let onShowingSubview = BrowserPageActions.sendToDevice.onShowingSubview; sandbox.stub(BrowserPageActions.sendToDevice, "onShowingSubview").callsFake((...args) => { this.numCall++ || (this.numCall = 1); onShowingSubview.call(BrowserPageActions.sendToDevice, ...args); testSendTabToDeviceMenu(this.numCall); }); @@ -309,23 +310,23 @@ add_task(async function sendToDevice_syn // The devices should be shown in the subview. let expectedItems = [ { className: "pageAction-sendToDevice-notReady", display: "none", disabled: true, }, ]; - for (let client of mockRemoteClients) { + for (let target of mockTargets) { expectedItems.push({ attrs: { - clientId: client.id, - label: client.name, - clientType: client.type, - tooltiptext: gSync.formatLastSyncDate(new Date(lastModifiedFixture * 1000)), + clientId: target.id, + label: target.name, + clientType: target.type, + tooltiptext: target.clientRecord ? gSync.formatLastSyncDate(new Date(lastModifiedFixture * 1000)) : "", }, }); } expectedItems.push( null, { attrs: { label: "Send to All Devices", @@ -400,18 +401,18 @@ add_task(async function sendToDevice_noD // Open a tab that's sendable. await BrowserTestUtils.withNewTab("http://example.com/", async () => { await promiseSyncReady(); const sandbox = sinon.sandbox.create(); sandbox.stub(gSync, "syncReady").get(() => true); sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false); sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN }); sandbox.stub(gSync, "isSendableURI").returns(true); - sandbox.stub(gSync, "remoteClients").get(() => []); - sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockRemoteClients.find(c => c.id == id).type); + sandbox.stub(gSync, "sendTabTargets").get(() => []); + sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type); let cleanUp = () => { sandbox.restore(); }; registerCleanupFunction(cleanUp); // Open the panel. await promisePageActionPanelOpen(); @@ -466,18 +467,18 @@ add_task(async function sendToDevice_dev // Open a tab that's sendable. await BrowserTestUtils.withNewTab("http://example.com/", async () => { await promiseSyncReady(); const sandbox = sinon.sandbox.create(); sandbox.stub(gSync, "syncReady").get(() => true); sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false); sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN }); sandbox.stub(gSync, "isSendableURI").returns(true); - sandbox.stub(gSync, "remoteClients").get(() => mockRemoteClients); - sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockRemoteClients.find(c => c.id == id).type); + sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets); + sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type); let cleanUp = () => { sandbox.restore(); }; registerCleanupFunction(cleanUp); // Open the panel. await promisePageActionPanelOpen(); @@ -494,22 +495,22 @@ add_task(async function sendToDevice_dev // The devices should be shown in the subview. let expectedItems = [ { className: "pageAction-sendToDevice-notReady", display: "none", disabled: true, }, ]; - for (let client of mockRemoteClients) { + for (let target of mockTargets) { expectedItems.push({ attrs: { - clientId: client.id, - label: client.name, - clientType: client.type, + clientId: target.id, + label: target.name, + clientType: target.type, }, }); } expectedItems.push( null, { attrs: { label: "Send to All Devices", @@ -532,18 +533,18 @@ add_task(async function sendToDevice_tit await BrowserTestUtils.withNewTab("http://example.com/a", async otherBrowser => { await BrowserTestUtils.withNewTab("http://example.com/b", async () => { await promiseSyncReady(); const sandbox = sinon.sandbox.create(); sandbox.stub(gSync, "syncReady").get(() => true); sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false); sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN }); sandbox.stub(gSync, "isSendableURI").returns(true); - sandbox.stub(gSync, "remoteClients").get(() => []); - sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockRemoteClients.find(c => c.id == id).type); + sandbox.stub(gSync, "sendTabTargets").get(() => []); + sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type); let cleanUp = () => { sandbox.restore(); }; registerCleanupFunction(cleanUp); // Open the panel. Only one tab is selected, so the action's title should // be "Send Tab to Device". @@ -589,18 +590,18 @@ add_task(async function sendToDevice_inU // Open a tab that's sendable. await BrowserTestUtils.withNewTab("http://example.com/", async () => { await promiseSyncReady(); const sandbox = sinon.sandbox.create(); sandbox.stub(gSync, "syncReady").get(() => true); sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false); sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN }); sandbox.stub(gSync, "isSendableURI").returns(true); - sandbox.stub(gSync, "remoteClients").get(() => mockRemoteClients); - sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockRemoteClients.find(c => c.id == id).type); + sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets); + sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type); let cleanUp = () => { sandbox.restore(); }; registerCleanupFunction(cleanUp); // Add Send to Device to the urlbar. let action = PageActions.actionForID("sendToDevice"); @@ -623,22 +624,22 @@ add_task(async function sendToDevice_inU // The devices should be shown in the subview. let expectedItems = [ { className: "pageAction-sendToDevice-notReady", display: "none", disabled: true, }, ]; - for (let client of mockRemoteClients) { + for (let target of mockTargets) { expectedItems.push({ attrs: { - clientId: client.id, - label: client.name, - clientType: client.type, + clientId: target.id, + label: target.name, + clientType: target.type, }, }); } expectedItems.push( null, { attrs: { label: "Send to All Devices",
--- a/services/fxaccounts/FxAccountsCommands.js +++ b/services/fxaccounts/FxAccountsCommands.js @@ -174,24 +174,19 @@ class SendTab { log.error("Error while invoking a send tab command.", error); report.failed.push({device, error}); } } return report; } // Returns true if the target device is compatible with FxA Commands Send tab. - async isDeviceCompatible(device) { - if (!Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true) || - !device.availableCommands || !device.availableCommands[COMMAND_SENDTAB]) { - return false; - } - const {kid: theirKid} = JSON.parse(device.availableCommands[COMMAND_SENDTAB]); - const ourKid = await this._getKid(); - return theirKid == ourKid; + isDeviceCompatible(device) { + return Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true) && + device.availableCommands && device.availableCommands[COMMAND_SENDTAB]; } // Handle incoming send tab payload, called by FxAccountsCommands. async handle(sender, {encrypted}) { if (!sender) { log.warn("Incoming tab is from an unknown device (maybe disconnected?)"); } const bytes = await this._decrypt(encrypted); @@ -203,30 +198,29 @@ class SendTab { id: sender ? sender.id : "", name: sender ? sender.name : "", }; const {title, url: uri} = data.entries[current]; log.info(`Tab received with FxA commands: ${title} from ${tabSender.name}.`); Observers.notify("fxaccounts:commands:open-uri", [{uri, title, sender: tabSender}]); } - async _getKid() { - let {kXCS} = await this._fxAccounts.getKeys(); - return kXCS; - } - async _encrypt(bytes, device) { let bundle = device.availableCommands[COMMAND_SENDTAB]; if (!bundle) { throw new Error(`Device ${device.id} does not have send tab keys.`); } + const {kSync, kXCS: ourKid} = await this._fxAccounts.getKeys(); + const {kid: theirKid} = JSON.parse(device.availableCommands[COMMAND_SENDTAB]); + if (theirKid != ourKid) { + throw new Error("Target Send Tab key ID is different from ours"); + } const json = JSON.parse(bundle); const wrapper = new CryptoWrapper(); wrapper.deserialize({payload: json}); - const {kSync} = await this._fxAccounts.getKeys(); const syncKeyBundle = BulkKeyBundle.fromHexKey(kSync); let {publicKey, authSecret} = await wrapper.decrypt(syncKeyBundle); authSecret = urlsafeBase64Decode(authSecret); publicKey = urlsafeBase64Decode(publicKey); const {ciphertext: encrypted} = await PushCrypto.encrypt(bytes, publicKey, authSecret); return urlsafeBase64Encode(encrypted); } @@ -273,27 +267,26 @@ class SendTab { if (!sendTabKeys) { sendTabKeys = await this._generateAndPersistKeys(); } // Strip the private key from the bundle to encrypt. const keyToEncrypt = { publicKey: sendTabKeys.publicKey, authSecret: sendTabKeys.authSecret, }; - const {kSync} = await this._fxAccounts.getSignedInUser(); - if (!kSync) { + const {kSync, kXCS} = await this._fxAccounts.getKeys(); + if (!kSync || !kXCS) { return null; } const wrapper = new CryptoWrapper(); wrapper.cleartext = keyToEncrypt; const keyBundle = BulkKeyBundle.fromHexKey(kSync); await wrapper.encrypt(keyBundle); - const kid = await this._getKid(); return JSON.stringify({ - kid, + kid: kXCS, IV: wrapper.IV, hmac: wrapper.hmac, ciphertext: wrapper.ciphertext, }); } } function urlsafeBase64Encode(buffer) {
--- a/services/fxaccounts/tests/xpcshell/test_commands.js +++ b/services/fxaccounts/tests/xpcshell/test_commands.js @@ -2,40 +2,25 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; ChromeUtils.import("resource://testing-common/Assert.jsm"); ChromeUtils.import("resource://gre/modules/FxAccountsCommands.js"); add_task(async function test_sendtab_isDeviceCompatible() { - const fxAccounts = { - getKeys() { - return { - kXCS: "abcd", - }; - }, - }; - const sendTab = new SendTab(null, fxAccounts); + const sendTab = new SendTab(null, null); let device = {name: "My device"}; - Assert.ok(!(await sendTab.isDeviceCompatible(device))); + Assert.ok(!sendTab.isDeviceCompatible(device)); device = {name: "My device", availableCommands: {}}; - Assert.ok(!(await sendTab.isDeviceCompatible(device))); + Assert.ok(!sendTab.isDeviceCompatible(device)); device = {name: "My device", availableCommands: { - "https://identity.mozilla.com/cmd/open-uri": JSON.stringify({ - kid: "dcba", - }), + "https://identity.mozilla.com/cmd/open-uri": "payload", }}; - Assert.ok(!(await sendTab.isDeviceCompatible(device))); - device = {name: "My device", availableCommands: { - "https://identity.mozilla.com/cmd/open-uri": JSON.stringify({ - kid: "abcd", - }), - }}; - Assert.ok((await sendTab.isDeviceCompatible(device))); + Assert.ok(sendTab.isDeviceCompatible(device)); }); add_task(async function test_sendtab_send() { const commands = { invoke: sinon.spy((cmd, device, payload) => { if (device.name == "Device 1") { throw new Error("Invoke error!"); }
--- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -45,16 +45,20 @@ ChromeUtils.defineModuleGetter(this, "ge const CLIENTS_TTL = 1814400; // 21 days const CLIENTS_TTL_REFRESH = 604800; // 7 days const STALE_CLIENT_REMOTE_AGE = 604800; // 7 days // TTL of the message sent to another device when sending a tab const NOTIFY_TAB_SENT_TTL_SECS = 1 * 3600; // 1 hour +// This is to avoid multiple sequential syncs ending up calling +// this expensive endpoint multiple times in a row. +const TIME_BETWEEN_FXA_DEVICES_FETCH_MS = 10 * 1000; + // Reasons behind sending collection_changed push notifications. const COLLECTION_MODIFIED_REASON_SENDTAB = "sendtab"; const COLLECTION_MODIFIED_REASON_FIRSTSYNC = "firstsync"; const SUPPORTED_PROTOCOL_VERSIONS = [SYNC_API_VERSION]; const LAST_MODIFIED_ON_PROCESS_COMMAND_PREF = "services.sync.clients.lastModifiedOnProcessCommands"; function hasDupeCommand(commands, action) { @@ -125,16 +129,20 @@ ClientEngine.prototype = { get lastRecordUpload() { return Svc.Prefs.get(this.name + ".lastRecordUpload", 0); }, set lastRecordUpload(value) { Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value)); }, + get fxaDevices() { + return this._fxaDevices; + }, + get remoteClients() { // return all non-stale clients for external consumption. return Object.values(this._store._remoteClients).filter(v => !v.stale); }, remoteClient(id) { let client = this._store._remoteClients[id]; return client && !client.stale ? client : null; @@ -322,17 +330,18 @@ ClientEngine.prototype = { async _removeClientCommands(clientId) { const allCommands = await this._readCommands(); delete allCommands[clientId]; await this._saveCommands(allCommands); }, async updateKnownStaleClients() { this._log.debug("Updating the known stale clients"); - await this._refreshKnownStaleClients(); + // _fetchFxADevices side effect updates this._knownStaleFxADeviceIds. + await this._fetchFxADevices(); let localFxADeviceId = await fxAccounts.getDeviceId(); // Process newer records first, so that if we hit a record with a device ID // we've seen before, we can mark it stale immediately. let clientList = Object.values(this._store._remoteClients).sort((a, b) => b.serverLastModified - a.serverLastModified); let seenDeviceIds = new Set([localFxADeviceId]); for (let client of clientList) { // Clients might not have an `fxaDeviceId` if they fail the FxA @@ -348,32 +357,40 @@ ClientEngine.prototype = { ` - duplicate device id ${client.fxaDeviceId}`); client.stale = true; } else { seenDeviceIds.add(client.fxaDeviceId); } } }, - // We assume that clients not present in the FxA Device Manager list have been - // disconnected and so are stale - async _refreshKnownStaleClients() { + async _fetchFxADevices() { + const now = new Date().getTime(); + if ((this._lastFxADevicesFetch || 0) + TIME_BETWEEN_FXA_DEVICES_FETCH_MS >= now) { + return; + } + const remoteClients = Object.values(this.remoteClients); + try { + this._fxaDevices = await this.fxAccounts.getDeviceList(); + for (const device of this._fxaDevices) { + device.clientRecord = remoteClients.find(c => c.fxaDeviceId == device.id); + } + } catch (e) { + this._log.error("Could not retrieve the FxA device list", e); + this._fxaDevices = []; + } + this._lastFxADevicesFetch = now; + + // We assume that clients not present in the FxA Device Manager list have been + // disconnected and so are stale this._log.debug("Refreshing the known stale clients list"); let localClients = Object.values(this._store._remoteClients) .filter(client => client.fxaDeviceId) // iOS client records don't have fxaDeviceId .map(client => client.fxaDeviceId); - let fxaClients; - try { - let deviceList = await this.fxAccounts.getDeviceList(); - fxaClients = deviceList.map(device => device.id); - } catch (ex) { - this._log.error("Could not retrieve the FxA device list", ex); - this._knownStaleFxADeviceIds = []; - return; - } + const fxaClients = this._fxaDevices.map(device => device.id); this._knownStaleFxADeviceIds = Utils.arraySub(localClients, fxaClients); }, async _syncStartup() { // Reupload new client record periodically. if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) { await this._tracker.addChangedID(this.localID); } @@ -381,21 +398,18 @@ ClientEngine.prototype = { }, async _processIncoming() { // Fetch all records from the server. await this.resetLastSync(); this._incomingClients = {}; try { await SyncEngine.prototype._processIncoming.call(this); - // Refresh the known stale clients list at startup and when we receive - // "device connected/disconnected" push notifications. - if (!this._knownStaleFxADeviceIds) { - await this._refreshKnownStaleClients(); - } + // Update FxA Device list. + await this._fetchFxADevices(); // Since clients are synced unconditionally, any records in the local store // that don't exist on the server must be for disconnected clients. Remove // them, so that we don't upload records with commands for clients that will // never see them. We also do this to filter out stale clients from the // tabs collection, since showing their list of tabs is confusing. for (let id in this._store._remoteClients) { if (!this._incomingClients[id]) { this._log.info(`Removing local state for deleted client ${id}`);
--- a/services/sync/tests/unit/test_clients_engine.js +++ b/services/sync/tests/unit/test_clients_engine.js @@ -43,16 +43,17 @@ function compareCommands(actual, expecte tweakedActual.map(elt => delete elt.flowID); deepEqual(tweakedActual, expected, description); // each item must have a unique flowID. let allIDs = new Set(actual.map(elt => elt.flowID).filter(fid => !!fid)); equal(allIDs.size, actual.length, "all items have unique IDs"); } async function syncClientsEngine(server) { + engine._lastFxADevicesFetch = 0; engine.lastModified = server.getCollection("foo", "clients").timestamp; await engine._sync(); } add_task(async function setup() { engine = Service.clientsEngine; }); @@ -1818,64 +1819,28 @@ add_task(async function device_disconnec }); add_task(async function update_known_stale_clients() { const makeFakeClient = (id) => ({ id, fxaDeviceId: `fxa-${id}` }); const clients = [makeFakeClient("one"), makeFakeClient("two"), makeFakeClient("three")]; const stubRemoteClients = sinon.stub(engine._store, "_remoteClients").get(() => { return clients; }); - const stubRefresh = sinon.stub(engine, "_refreshKnownStaleClients", () => { + const stubFetchFxADevices = sinon.stub(engine, "_fetchFxADevices", () => { engine._knownStaleFxADeviceIds = ["fxa-one", "fxa-two"]; }); engine._knownStaleFxADeviceIds = null; await engine.updateKnownStaleClients(); ok(clients[0].stale); ok(clients[1].stale); ok(!clients[2].stale); stubRemoteClients.restore(); - stubRefresh.restore(); -}); - -add_task(async function process_incoming_refreshes_known_stale_clients() { - const stubProcessIncoming = sinon.stub(SyncEngine.prototype, "_processIncoming"); - const stubRefresh = sinon.stub(engine, "_refreshKnownStaleClients", () => { - engine._knownStaleFxADeviceIds = ["one", "two"]; - }); - - engine._knownStaleFxADeviceIds = null; - await engine._processIncoming(); - ok(stubRefresh.calledOnce, "Should refresh the known stale clients"); - stubRefresh.reset(); - - await engine._processIncoming(); - ok(stubRefresh.notCalled, "Should not refresh the known stale clients since it's already populated"); - - stubProcessIncoming.restore(); - stubRefresh.restore(); -}); - -add_task(async function process_incoming_refreshes_known_stale_clients() { - Services.prefs.clearUserPref("services.sync.clients.lastModifiedOnProcessCommands"); - engine._localClientLastModified = Math.round(Date.now() / 1000); - - const stubRemoveLocalCommand = sinon.stub(engine, "removeLocalCommand"); - const tabProcessedSpy = sinon.spy(engine, "_handleDisplayURIs"); - engine.localCommands = [{ command: "displayURI", args: ["https://foo.bar", "fxaid1", "foo"] }]; - - await engine.processIncomingCommands(); - ok(tabProcessedSpy.calledOnce); - // Let's say we failed to upload and we end up calling processIncomingCommands again - await engine.processIncomingCommands(); - ok(tabProcessedSpy.calledOnce); - - tabProcessedSpy.restore(); - stubRemoveLocalCommand.restore(); + stubFetchFxADevices.restore(); }); add_task(async function test_create_record_command_limit() { await engine._store.wipe(); await generateNewKeys(Service.collectionKeys); let server = await serverForFoo(engine); await SyncTestingInfrastructure(server);