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:
Edouard Oger
2018-11-16 03:03:13 +00:00
parent 24bf177fea
commit f30ef5f47d
9 changed files with 136 additions and 179 deletions

View File

@@ -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);

View File

@@ -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");

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,
},
});
}

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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

View File

@@ -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() {