Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / services / fxaccounts / FxAccountsPush.sys.mjs
blobe3e5f32de5064303482b66a4bc470cbadf11874f
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { Async } from "resource://services-common/async.sys.mjs";
7 import {
8   FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
9   ONLOGOUT_NOTIFICATION,
10   ON_ACCOUNT_DESTROYED_NOTIFICATION,
11   ON_COLLECTION_CHANGED_NOTIFICATION,
12   ON_COMMAND_RECEIVED_NOTIFICATION,
13   ON_DEVICE_CONNECTED_NOTIFICATION,
14   ON_DEVICE_DISCONNECTED_NOTIFICATION,
15   ON_PASSWORD_CHANGED_NOTIFICATION,
16   ON_PASSWORD_RESET_NOTIFICATION,
17   ON_PROFILE_CHANGE_NOTIFICATION,
18   ON_PROFILE_UPDATED_NOTIFICATION,
19   ON_VERIFY_LOGIN_NOTIFICATION,
20   log,
21 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
23 /**
24  * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
25  *
26  * @param [options]
27  *        Object, custom options that used for testing
28  * @constructor
29  */
30 export function FxAccountsPushService(options = {}) {
31   this.log = log;
33   if (options.log) {
34     // allow custom log for testing purposes
35     this.log = options.log;
36   }
38   this.log.debug("FxAccountsPush loading service");
39   this.wrappedJSObject = this;
40   this.initialize(options);
43 FxAccountsPushService.prototype = {
44   /**
45    * Helps only initialize observers once.
46    */
47   _initialized: false,
48   /**
49    * Instance of the nsIPushService or a mocked object.
50    */
51   pushService: null,
52   /**
53    * Instance of FxAccountsInternal or a mocked object.
54    */
55   fxai: null,
56   /**
57    * Component ID of this service, helps register this component.
58    */
59   classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
60   /**
61    * Register used interfaces in this service
62    */
63   QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
64   /**
65    * Initialize the service and register all the required observers.
66    *
67    * @param [options]
68    */
69   initialize(options) {
70     if (this._initialized) {
71       return false;
72     }
74     this._initialized = true;
76     if (options.pushService) {
77       this.pushService = options.pushService;
78     } else {
79       this.pushService = Cc["@mozilla.org/push/Service;1"].getService(
80         Ci.nsIPushService
81       );
82     }
84     if (options.fxai) {
85       this.fxai = options.fxai;
86     } else {
87       const { getFxAccountsSingleton } = ChromeUtils.importESModule(
88         "resource://gre/modules/FxAccounts.sys.mjs"
89       );
90       const fxAccounts = getFxAccountsSingleton();
91       this.fxai = fxAccounts._internal;
92     }
94     this.asyncObserver = Async.asyncObserver(this, this.log);
95     // We use an async observer because a device waking up can
96     // observe multiple "Send Tab received" push notifications at the same time.
97     // The way these notifications are handled is as follows:
98     // Read index from storage, make network request, update the index.
99     // You can imagine what happens when multiple calls race: we load
100     // the same index multiple times and receive the same exact tabs, multiple times.
101     // The async observer will ensure we make these network requests serially.
102     Services.obs.addObserver(this.asyncObserver, this.pushService.pushTopic);
103     Services.obs.addObserver(
104       this.asyncObserver,
105       this.pushService.subscriptionChangeTopic
106     );
107     Services.obs.addObserver(this.asyncObserver, ONLOGOUT_NOTIFICATION);
109     this.log.debug("FxAccountsPush initialized");
110     return true;
111   },
112   /**
113    * Registers a new endpoint with the Push Server
114    *
115    * @returns {Promise}
116    *          Promise always resolves with a subscription or a null if failed to subscribe.
117    */
118   registerPushEndpoint() {
119     this.log.trace("FxAccountsPush registerPushEndpoint");
121     return new Promise(resolve => {
122       this.pushService.subscribe(
123         FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
124         Services.scriptSecurityManager.getSystemPrincipal(),
125         (result, subscription) => {
126           if (Components.isSuccessCode(result)) {
127             this.log.debug("FxAccountsPush got subscription");
128             resolve(subscription);
129           } else {
130             this.log.warn("FxAccountsPush failed to subscribe", result);
131             resolve(null);
132           }
133         }
134       );
135     });
136   },
137   /**
138    * Async observer interface to listen to push messages, changes and logout.
139    *
140    * @param subject
141    * @param topic
142    * @param data
143    * @returns {Promise}
144    */
145   async observe(subject, topic, data) {
146     try {
147       this.log.trace(
148         `observed topic=${topic}, data=${data}, subject=${subject}`
149       );
150       switch (topic) {
151         case this.pushService.pushTopic:
152           if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
153             let message = subject.QueryInterface(Ci.nsIPushMessage);
154             await this._onPushMessage(message);
155           }
156           break;
157         case this.pushService.subscriptionChangeTopic:
158           if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
159             await this._onPushSubscriptionChange();
160           }
161           break;
162         case ONLOGOUT_NOTIFICATION:
163           // user signed out, we need to stop polling the Push Server
164           await this.unsubscribe();
165           break;
166       }
167     } catch (err) {
168       this.log.error(err);
169     }
170   },
172   /**
173    * Fired when the Push server sends a notification.
174    *
175    * @private
176    * @returns {Promise}
177    */
178   async _onPushMessage(message) {
179     this.log.trace("FxAccountsPushService _onPushMessage");
180     if (!message.data) {
181       // Use the empty signal to check the verification state of the account right away
182       this.log.debug("empty push message - checking account status");
183       this.fxai.checkVerificationStatus();
184       return;
185     }
186     let payload = message.data.json();
187     this.log.debug(`push command: ${payload.command}`);
188     switch (payload.command) {
189       case ON_COMMAND_RECEIVED_NOTIFICATION:
190         await this.fxai.commands.pollDeviceCommands(payload.data.index);
191         break;
192       case ON_DEVICE_CONNECTED_NOTIFICATION:
193         Services.obs.notifyObservers(
194           null,
195           ON_DEVICE_CONNECTED_NOTIFICATION,
196           payload.data.deviceName
197         );
198         break;
199       case ON_DEVICE_DISCONNECTED_NOTIFICATION:
200         this.fxai._handleDeviceDisconnection(payload.data.id);
201         return;
202       case ON_PROFILE_UPDATED_NOTIFICATION:
203         // We already have a "profile updated" notification sent via WebChannel,
204         // let's just re-use that.
205         Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
206         return;
207       case ON_PASSWORD_CHANGED_NOTIFICATION:
208       case ON_PASSWORD_RESET_NOTIFICATION:
209         this._onPasswordChanged();
210         return;
211       case ON_ACCOUNT_DESTROYED_NOTIFICATION:
212         this.fxai._handleAccountDestroyed(payload.data.uid);
213         return;
214       case ON_COLLECTION_CHANGED_NOTIFICATION:
215         Services.obs.notifyObservers(
216           null,
217           ON_COLLECTION_CHANGED_NOTIFICATION,
218           payload.data.collections
219         );
220         return;
221       case ON_VERIFY_LOGIN_NOTIFICATION:
222         Services.obs.notifyObservers(
223           null,
224           ON_VERIFY_LOGIN_NOTIFICATION,
225           JSON.stringify(payload.data)
226         );
227         break;
228       default:
229         this.log.warn("FxA Push command unrecognized: " + payload.command);
230     }
231   },
232   /**
233    * Check the FxA session status after a password change/reset event.
234    * If the session is invalid, reset credentials and notify listeners of
235    * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed
236    *
237    * @returns {Promise}
238    * @private
239    */
240   _onPasswordChanged() {
241     return this.fxai.withCurrentAccountState(async state => {
242       return this.fxai.checkAccountStatus(state);
243     });
244   },
245   /**
246    * Fired when the Push server drops a subscription, or the subscription identifier changes.
247    *
248    * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages
249    *
250    * @returns {Promise}
251    * @private
252    */
253   _onPushSubscriptionChange() {
254     this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
255     return this.fxai.updateDeviceRegistration();
256   },
257   /**
258    * Unsubscribe from the Push server
259    *
260    * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
261    *
262    * @returns {Promise} - The promise resolves with a bool to indicate if we successfully unsubscribed.
263    *                      The promise never rejects.
264    * @private
265    */
266   unsubscribe() {
267     this.log.trace("FxAccountsPushService unsubscribe");
268     return new Promise(resolve => {
269       this.pushService.unsubscribe(
270         FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
271         Services.scriptSecurityManager.getSystemPrincipal(),
272         (result, ok) => {
273           if (Components.isSuccessCode(result)) {
274             if (ok === true) {
275               this.log.debug("FxAccountsPushService unsubscribed");
276             } else {
277               this.log.debug(
278                 "FxAccountsPushService had no subscription to unsubscribe"
279               );
280             }
281           } else {
282             this.log.warn(
283               "FxAccountsPushService failed to unsubscribe",
284               result
285             );
286           }
287           return resolve(ok);
288         }
289       );
290     });
291   },
293   /**
294    * Get our Push server subscription.
295    *
296    * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#getSubscription()
297    *
298    * @returns {Promise} - resolves with the subscription or null. Never rejects.
299    */
300   getSubscription() {
301     return new Promise(resolve => {
302       this.pushService.getSubscription(
303         FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
304         Services.scriptSecurityManager.getSystemPrincipal(),
305         (result, subscription) => {
306           if (!subscription) {
307             this.log.info("FxAccountsPushService no subscription found");
308             return resolve(null);
309           }
310           return resolve(subscription);
311         }
312       );
313     });
314   },