Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Async } from "resource://services-common/async.sys.mjs";
import {
FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
ONLOGOUT_NOTIFICATION,
ON_ACCOUNT_DESTROYED_NOTIFICATION,
ON_COLLECTION_CHANGED_NOTIFICATION,
ON_COMMAND_RECEIVED_NOTIFICATION,
ON_DEVICE_CONNECTED_NOTIFICATION,
ON_DEVICE_DISCONNECTED_NOTIFICATION,
ON_PASSWORD_CHANGED_NOTIFICATION,
ON_PASSWORD_RESET_NOTIFICATION,
ON_PROFILE_CHANGE_NOTIFICATION,
ON_PROFILE_UPDATED_NOTIFICATION,
ON_VERIFY_LOGIN_NOTIFICATION,
log,
/**
* FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
*
* @param [options]
* Object, custom options that used for testing
* @constructor
*/
export function FxAccountsPushService(options = {}) {
this.log = log;
if (options.log) {
// allow custom log for testing purposes
this.log = options.log;
}
this.log.debug("FxAccountsPush loading service");
this.wrappedJSObject = this;
this.initialize(options);
}
FxAccountsPushService.prototype = {
/**
* Helps only initialize observers once.
*/
_initialized: false,
/**
* Instance of the nsIPushService or a mocked object.
*/
pushService: null,
/**
* Instance of FxAccountsInternal or a mocked object.
*/
fxai: null,
/**
* Component ID of this service, helps register this component.
*/
classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
/**
* Register used interfaces in this service
*/
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
/**
* Initialize the service and register all the required observers.
*
* @param [options]
*/
initialize(options) {
if (this._initialized) {
return false;
}
this._initialized = true;
if (options.pushService) {
this.pushService = options.pushService;
} else {
this.pushService = Cc["@mozilla.org/push/Service;1"].getService(
Ci.nsIPushService
);
}
if (options.fxai) {
this.fxai = options.fxai;
} else {
const { getFxAccountsSingleton } = ChromeUtils.importESModule(
);
const fxAccounts = getFxAccountsSingleton();
this.fxai = fxAccounts._internal;
}
this.asyncObserver = Async.asyncObserver(this, this.log);
// We use an async observer because a device waking up can
// observe multiple "Send Tab received" push notifications at the same time.
// The way these notifications are handled is as follows:
// Read index from storage, make network request, update the index.
// You can imagine what happens when multiple calls race: we load
// the same index multiple times and receive the same exact tabs, multiple times.
// The async observer will ensure we make these network requests serially.
Services.obs.addObserver(this.asyncObserver, this.pushService.pushTopic);
Services.obs.addObserver(
this.asyncObserver,
this.pushService.subscriptionChangeTopic
);
Services.obs.addObserver(this.asyncObserver, ONLOGOUT_NOTIFICATION);
this.log.debug("FxAccountsPush initialized");
return true;
},
/**
* Registers a new endpoint with the Push Server
*
* @returns {Promise}
* Promise always resolves with a subscription or a null if failed to subscribe.
*/
registerPushEndpoint() {
this.log.trace("FxAccountsPush registerPushEndpoint");
return new Promise(resolve => {
this.pushService.subscribe(
FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, subscription) => {
if (Components.isSuccessCode(result)) {
this.log.debug("FxAccountsPush got subscription");
resolve(subscription);
} else {
this.log.warn("FxAccountsPush failed to subscribe", result);
resolve(null);
}
}
);
});
},
/**
* Async observer interface to listen to push messages, changes and logout.
*
* @param subject
* @param topic
* @param data
* @returns {Promise}
*/
async observe(subject, topic, data) {
try {
this.log.trace(
`observed topic=${topic}, data=${data}, subject=${subject}`
);
switch (topic) {
case this.pushService.pushTopic:
if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
let message = subject.QueryInterface(Ci.nsIPushMessage);
await this._onPushMessage(message);
}
break;
case this.pushService.subscriptionChangeTopic:
if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
await this._onPushSubscriptionChange();
}
break;
case ONLOGOUT_NOTIFICATION:
// user signed out, we need to stop polling the Push Server
await this.unsubscribe();
break;
}
} catch (err) {
this.log.error(err);
}
},
/**
* Fired when the Push server sends a notification.
*
* @private
* @returns {Promise}
*/
async _onPushMessage(message) {
this.log.trace("FxAccountsPushService _onPushMessage");
if (!message.data) {
// Use the empty signal to check the verification state of the account right away
this.log.debug("empty push message - checking account status");
this.fxai.checkVerificationStatus();
return;
}
let payload = message.data.json();
this.log.debug(`push command: ${payload.command}`);
switch (payload.command) {
case ON_COMMAND_RECEIVED_NOTIFICATION:
await this.fxai.commands.pollDeviceCommands(payload.data.index);
break;
case ON_DEVICE_CONNECTED_NOTIFICATION:
Services.obs.notifyObservers(
null,
ON_DEVICE_CONNECTED_NOTIFICATION,
payload.data.deviceName
);
break;
case ON_DEVICE_DISCONNECTED_NOTIFICATION:
this.fxai._handleDeviceDisconnection(payload.data.id);
return;
case ON_PROFILE_UPDATED_NOTIFICATION:
// We already have a "profile updated" notification sent via WebChannel,
// let's just re-use that.
Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
return;
case ON_PASSWORD_CHANGED_NOTIFICATION:
case ON_PASSWORD_RESET_NOTIFICATION:
this._onPasswordChanged();
return;
case ON_ACCOUNT_DESTROYED_NOTIFICATION:
this.fxai._handleAccountDestroyed(payload.data.uid);
return;
case ON_COLLECTION_CHANGED_NOTIFICATION:
Services.obs.notifyObservers(
null,
ON_COLLECTION_CHANGED_NOTIFICATION,
payload.data.collections
);
return;
case ON_VERIFY_LOGIN_NOTIFICATION:
Services.obs.notifyObservers(
null,
ON_VERIFY_LOGIN_NOTIFICATION,
JSON.stringify(payload.data)
);
break;
default:
this.log.warn("FxA Push command unrecognized: " + payload.command);
}
},
/**
* Check the FxA session status after a password change/reset event.
* If the session is invalid, reset credentials and notify listeners of
* ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed
*
* @returns {Promise}
* @private
*/
_onPasswordChanged() {
return this.fxai.withCurrentAccountState(async state => {
return this.fxai.checkAccountStatus(state);
});
},
/**
* Fired when the Push server drops a subscription, or the subscription identifier changes.
*
*
* @returns {Promise}
* @private
*/
_onPushSubscriptionChange() {
this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
return this.fxai.updateDeviceRegistration();
},
/**
* Unsubscribe from the Push server
*
*
* @returns {Promise} - The promise resolves with a bool to indicate if we successfully unsubscribed.
* The promise never rejects.
* @private
*/
unsubscribe() {
this.log.trace("FxAccountsPushService unsubscribe");
return new Promise(resolve => {
this.pushService.unsubscribe(
FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, ok) => {
if (Components.isSuccessCode(result)) {
if (ok === true) {
this.log.debug("FxAccountsPushService unsubscribed");
} else {
this.log.debug(
"FxAccountsPushService had no subscription to unsubscribe"
);
}
} else {
this.log.warn(
"FxAccountsPushService failed to unsubscribe",
result
);
}
return resolve(ok);
}
);
});
},
/**
* Get our Push server subscription.
*
*
* @returns {Promise} - resolves with the subscription or null. Never rejects.
*/
getSubscription() {
return new Promise(resolve => {
this.pushService.getSubscription(
FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, subscription) => {
if (!subscription) {
this.log.info("FxAccountsPushService no subscription found");
return resolve(null);
}
return resolve(subscription);
}
);
});
},
};