Bug 1507294 - Display all compatible FxA devices in send tab menu. r=markh
Differential Revision: https://phabricator.services.mozilla.com/D11955
This commit is contained in:
@@ -60,9 +60,10 @@ var gSync = {
|
||||
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() {
|
||||
@@ -316,25 +317,16 @@ var gSync = {
|
||||
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) {
|
||||
@@ -343,12 +335,11 @@ var gSync = {
|
||||
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) {
|
||||
@@ -363,7 +354,7 @@ var gSync = {
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -385,7 +376,7 @@ var gSync = {
|
||||
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);
|
||||
@@ -403,6 +394,8 @@ var gSync = {
|
||||
// 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 {
|
||||
@@ -413,36 +406,42 @@ var gSync = {
|
||||
|
||||
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);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
"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();
|
||||
@@ -14,7 +14,7 @@ add_task(async function setup() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -32,7 +32,7 @@ add_task(async function test_page_contextmenu() {
|
||||
});
|
||||
|
||||
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")
|
||||
@@ -59,7 +59,7 @@ add_task(async function test_link_contextmenu() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -77,7 +77,7 @@ add_task(async function test_page_contextmenu_no_remote_clients() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -92,7 +92,7 @@ add_task(async function test_page_contextmenu_one_remote_client() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -105,7 +105,7 @@ add_task(async function test_page_contextmenu_not_sendable() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -118,7 +118,7 @@ add_task(async function test_page_contextmenu_not_synced_yet() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -131,7 +131,7 @@ add_task(async function test_page_contextmenu_sync_not_ready_configured() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -148,7 +148,7 @@ add_task(async function test_page_contextmenu_sync_not_ready_other_state() {
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -167,7 +167,7 @@ add_task(async function test_page_contextmenu_unconfigured() {
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
@@ -7,7 +7,7 @@ const chrome_base = "chrome://mochitests/content/browser/browser/base/content/te
|
||||
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;
|
||||
|
||||
@@ -35,7 +35,7 @@ add_task(async function setup() {
|
||||
});
|
||||
|
||||
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")
|
||||
@@ -55,7 +55,7 @@ add_task(async function test_tab_contextmenu() {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -66,7 +66,7 @@ add_task(async function test_tab_contextmenu_unconfigured() {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -77,7 +77,7 @@ add_task(async function test_tab_contextmenu_not_sendable() {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -88,7 +88,7 @@ add_task(async function test_tab_contextmenu_not_synced_yet() {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -99,7 +99,7 @@ add_task(async function test_tab_contextmenu_sync_not_ready_configured() {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -14,11 +14,11 @@ function promiseSyncReady() {
|
||||
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;
|
||||
|
||||
@@ -8,10 +8,11 @@ registerCleanupFunction(function() {
|
||||
});
|
||||
|
||||
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() {
|
||||
@@ -268,8 +269,8 @@ add_task(async function sendToDevice_syncNotReady_configured() {
|
||||
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;
|
||||
@@ -314,13 +315,13 @@ add_task(async function sendToDevice_syncNotReady_configured() {
|
||||
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)) : "",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -405,8 +406,8 @@ add_task(async function sendToDevice_noDevices() {
|
||||
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();
|
||||
@@ -471,8 +472,8 @@ add_task(async function sendToDevice_devices() {
|
||||
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();
|
||||
@@ -499,12 +500,12 @@ add_task(async function sendToDevice_devices() {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -537,8 +538,8 @@ add_task(async function sendToDevice_title() {
|
||||
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();
|
||||
@@ -594,8 +595,8 @@ add_task(async function sendToDevice_inUrlbar() {
|
||||
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();
|
||||
@@ -628,12 +629,12 @@ add_task(async function sendToDevice_inUrlbar() {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,14 +179,9 @@ class SendTab {
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -208,20 +203,19 @@ class SendTab {
|
||||
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);
|
||||
@@ -278,17 +272,16 @@ class SendTab {
|
||||
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,
|
||||
|
||||
@@ -7,30 +7,15 @@ 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() {
|
||||
|
||||
@@ -50,6 +50,10 @@ 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";
|
||||
@@ -130,6 +134,10 @@ ClientEngine.prototype = {
|
||||
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);
|
||||
@@ -327,7 +335,8 @@ ClientEngine.prototype = {
|
||||
|
||||
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.
|
||||
@@ -353,22 +362,30 @@ ClientEngine.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
// 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);
|
||||
},
|
||||
|
||||
@@ -386,11 +403,8 @@ ClientEngine.prototype = {
|
||||
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
|
||||
|
||||
@@ -48,6 +48,7 @@ function compareCommands(actual, expected, description) {
|
||||
}
|
||||
|
||||
async function syncClientsEngine(server) {
|
||||
engine._lastFxADevicesFetch = 0;
|
||||
engine.lastModified = server.getCollection("foo", "clients").timestamp;
|
||||
await engine._sync();
|
||||
}
|
||||
@@ -1823,7 +1824,7 @@ add_task(async function update_known_stale_clients() {
|
||||
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"];
|
||||
});
|
||||
|
||||
@@ -1834,43 +1835,7 @@ add_task(async function update_known_stale_clients() {
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user