Blame dom/push/PushService.jsm

Packit f0b94e
/* jshint moz: true, esnext: true */
Packit f0b94e
/* This Source Code Form is subject to the terms of the Mozilla Public
Packit f0b94e
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
Packit f0b94e
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
Packit f0b94e
Packit f0b94e
"use strict";
Packit f0b94e
Packit f0b94e
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
Packit f0b94e
ChromeUtils.import("resource://gre/modules/Preferences.jsm");
Packit f0b94e
ChromeUtils.import("resource://gre/modules/Services.jsm");
Packit f0b94e
ChromeUtils.import("resource://gre/modules/Timer.jsm");
Packit f0b94e
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
Packit f0b94e
Packit f0b94e
const {
Packit f0b94e
  PushCrypto,
Packit f0b94e
  getCryptoParams,
Packit f0b94e
  CryptoError,
Packit f0b94e
} = ChromeUtils.import("resource://gre/modules/PushCrypto.jsm");
Packit f0b94e
const {PushDB} = ChromeUtils.import("resource://gre/modules/PushDB.jsm");
Packit f0b94e
Packit f0b94e
const CONNECTION_PROTOCOLS = (function() {
Packit f0b94e
  if ('android' != AppConstants.MOZ_WIDGET_TOOLKIT) {
Packit f0b94e
    const {PushServiceWebSocket} = ChromeUtils.import("resource://gre/modules/PushServiceWebSocket.jsm");
Packit f0b94e
    const {PushServiceHttp2} = ChromeUtils.import("resource://gre/modules/PushServiceHttp2.jsm");
Packit f0b94e
    return [PushServiceWebSocket, PushServiceHttp2];
Packit f0b94e
  } else {
Packit f0b94e
    const {PushServiceAndroidGCM} = ChromeUtils.import("resource://gre/modules/PushServiceAndroidGCM.jsm");
Packit f0b94e
    return [PushServiceAndroidGCM];
Packit f0b94e
  }
Packit f0b94e
})();
Packit f0b94e
Packit f0b94e
XPCOMUtils.defineLazyServiceGetter(this, "gPushNotifier",
Packit f0b94e
                                   "@mozilla.org/push/Notifier;1",
Packit f0b94e
                                   "nsIPushNotifier");
Packit f0b94e
Packit f0b94e
var EXPORTED_SYMBOLS = ["PushService"];
Packit f0b94e
Packit f0b94e
XPCOMUtils.defineLazyGetter(this, "console", () => {
Packit f0b94e
  let {ConsoleAPI} = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
Packit f0b94e
  return new ConsoleAPI({
Packit f0b94e
    maxLogLevelPref: "dom.push.loglevel",
Packit f0b94e
    prefix: "PushService",
Packit f0b94e
  });
Packit f0b94e
});
Packit f0b94e
Packit f0b94e
const prefs = new Preferences("dom.push.");
Packit f0b94e
Packit f0b94e
const PUSH_SERVICE_UNINIT = 0;
Packit f0b94e
const PUSH_SERVICE_INIT = 1; // No serverURI
Packit f0b94e
const PUSH_SERVICE_ACTIVATING = 2;//activating db
Packit f0b94e
const PUSH_SERVICE_CONNECTION_DISABLE = 3;
Packit f0b94e
const PUSH_SERVICE_ACTIVE_OFFLINE = 4;
Packit f0b94e
const PUSH_SERVICE_RUNNING = 5;
Packit f0b94e
Packit f0b94e
/**
Packit f0b94e
 * State is change only in couple of functions:
Packit f0b94e
 *   init - change state to PUSH_SERVICE_INIT if state was PUSH_SERVICE_UNINIT
Packit f0b94e
 *   changeServerURL - change state to PUSH_SERVICE_ACTIVATING if serverURL
Packit f0b94e
 *                     present or PUSH_SERVICE_INIT if not present.
Packit f0b94e
 *   changeStateConnectionEnabledEvent - it is call on pref change or during
Packit f0b94e
 *                                       the service activation and it can
Packit f0b94e
 *                                       change state to
Packit f0b94e
 *                                       PUSH_SERVICE_CONNECTION_DISABLE
Packit f0b94e
 *   changeStateOfflineEvent - it is called when offline state changes or during
Packit f0b94e
 *                             the service activation and it change state to
Packit f0b94e
 *                             PUSH_SERVICE_ACTIVE_OFFLINE or
Packit f0b94e
 *                             PUSH_SERVICE_RUNNING.
Packit f0b94e
 *   uninit - change state to PUSH_SERVICE_UNINIT.
Packit f0b94e
 **/
Packit f0b94e
Packit f0b94e
// This is for starting and stopping service.
Packit f0b94e
const STARTING_SERVICE_EVENT = 0;
Packit f0b94e
const CHANGING_SERVICE_EVENT = 1;
Packit f0b94e
const STOPPING_SERVICE_EVENT = 2;
Packit f0b94e
const UNINIT_EVENT = 3;
Packit f0b94e
Packit f0b94e
/**
Packit f0b94e
 * Annotates an error with an XPCOM result code. We use this helper
Packit f0b94e
 * instead of `Components.Exception` because the latter can assert in
Packit f0b94e
 * `nsXPCComponents_Exception::HasInstance` when inspected at shutdown.
Packit f0b94e
 */
Packit f0b94e
function errorWithResult(message, result = Cr.NS_ERROR_FAILURE) {
Packit f0b94e
  let error = new Error(message);
Packit f0b94e
  error.result = result;
Packit f0b94e
  return error;
Packit f0b94e
}
Packit f0b94e
Packit f0b94e
/**
Packit f0b94e
 * Copied from ForgetAboutSite.jsm.
Packit f0b94e
 *
Packit f0b94e
 * Returns true if the string passed in is part of the root domain of the
Packit f0b94e
 * current string.  For example, if this is "www.mozilla.org", and we pass in
Packit f0b94e
 * "mozilla.org", this will return true.  It would return false the other way
Packit f0b94e
 * around.
Packit f0b94e
 */
Packit f0b94e
function hasRootDomain(str, aDomain)
Packit f0b94e
{
Packit f0b94e
  let index = str.indexOf(aDomain);
Packit f0b94e
  // If aDomain is not found, we know we do not have it as a root domain.
Packit f0b94e
  if (index == -1)
Packit f0b94e
    return false;
Packit f0b94e
Packit f0b94e
  // If the strings are the same, we obviously have a match.
Packit f0b94e
  if (str == aDomain)
Packit f0b94e
    return true;
Packit f0b94e
Packit f0b94e
  // Otherwise, we have aDomain as our root domain iff the index of aDomain is
Packit f0b94e
  // aDomain.length subtracted from our length and (since we do not have an
Packit f0b94e
  // exact match) the character before the index is a dot or slash.
Packit f0b94e
  let prevChar = str[index - 1];
Packit f0b94e
  return (index == (str.length - aDomain.length)) &&
Packit f0b94e
         (prevChar == "." || prevChar == "/");
Packit f0b94e
}
Packit f0b94e
Packit f0b94e
/**
Packit f0b94e
 * The implementation of the push system. It uses WebSockets
Packit f0b94e
 * (PushServiceWebSocket) to communicate with the server and PushDB (IndexedDB)
Packit f0b94e
 * for persistence.
Packit f0b94e
 */
Packit f0b94e
var PushService = {
Packit f0b94e
  _service: null,
Packit f0b94e
  _state: PUSH_SERVICE_UNINIT,
Packit f0b94e
  _db: null,
Packit f0b94e
  _options: null,
Packit f0b94e
  _visibleNotifications: new Map(),
Packit f0b94e
Packit f0b94e
  // Callback that is called after attempting to
Packit f0b94e
  // reduce the quota for a record. Used for testing purposes.
Packit f0b94e
  _updateQuotaTestCallback: null,
Packit f0b94e
Packit f0b94e
  // Set of timeout ID of tasks to reduce quota.
Packit f0b94e
  _updateQuotaTimeouts: new Set(),
Packit f0b94e
Packit f0b94e
  // When serverURI changes (this is used for testing), db is cleaned up and a
Packit f0b94e
  // a new db is started. This events must be sequential.
Packit f0b94e
  _stateChangeProcessQueue: null,
Packit f0b94e
  _stateChangeProcessEnqueue: function(op) {
Packit f0b94e
    if (!this._stateChangeProcessQueue) {
Packit f0b94e
      this._stateChangeProcessQueue = Promise.resolve();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    this._stateChangeProcessQueue = this._stateChangeProcessQueue
Packit f0b94e
      .then(op)
Packit f0b94e
      .catch(error => {
Packit f0b94e
        console.error(
Packit f0b94e
          "stateChangeProcessEnqueue: Error transitioning state", error);
Packit f0b94e
        return this._shutdownService();
Packit f0b94e
      })
Packit f0b94e
      .catch(error => {
Packit f0b94e
        console.error(
Packit f0b94e
          "stateChangeProcessEnqueue: Error shutting down service", error);
Packit f0b94e
      });
Packit f0b94e
    return this._stateChangeProcessQueue;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  // Pending request. If a worker try to register for the same scope again, do
Packit f0b94e
  // not send a new registration request. Therefore we need queue of pending
Packit f0b94e
  // register requests. This is the list of scopes which pending registration.
Packit f0b94e
  _pendingRegisterRequest: {},
Packit f0b94e
  _notifyActivated: null,
Packit f0b94e
  _activated: null,
Packit f0b94e
  _checkActivated: function() {
Packit f0b94e
    if (this._state < PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      return Promise.reject(new Error("Push service not active"));
Packit f0b94e
    } else if (this._state > PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      return Promise.resolve();
Packit f0b94e
    } else {
Packit f0b94e
      return (this._activated) ? this._activated :
Packit f0b94e
                                 this._activated = new Promise((res, rej) =>
Packit f0b94e
                                   this._notifyActivated = {resolve: res,
Packit f0b94e
                                                            reject: rej});
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _makePendingKey: function(aPageRecord) {
Packit f0b94e
    return aPageRecord.scope + "|" + aPageRecord.originAttributes;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _lookupOrPutPendingRequest: function(aPageRecord) {
Packit f0b94e
    let key = this._makePendingKey(aPageRecord);
Packit f0b94e
    if (this._pendingRegisterRequest[key]) {
Packit f0b94e
      return this._pendingRegisterRequest[key];
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    return this._pendingRegisterRequest[key] = this._registerWithServer(aPageRecord);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _deletePendingRequest: function(aPageRecord) {
Packit f0b94e
    let key = this._makePendingKey(aPageRecord);
Packit f0b94e
    if (this._pendingRegisterRequest[key]) {
Packit f0b94e
      delete this._pendingRegisterRequest[key];
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _setState: function(aNewState) {
Packit f0b94e
    console.debug("setState()", "new state", aNewState, "old state", this._state);
Packit f0b94e
Packit f0b94e
    if (this._state == aNewState) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    if (this._state == PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      // It is not important what is the new state as soon as we leave
Packit f0b94e
      // PUSH_SERVICE_ACTIVATING
Packit f0b94e
      if (this._notifyActivated) {
Packit f0b94e
        if (aNewState < PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
          this._notifyActivated.reject(new Error("Push service not active"));
Packit f0b94e
        } else {
Packit f0b94e
          this._notifyActivated.resolve();
Packit f0b94e
        }
Packit f0b94e
      }
Packit f0b94e
      this._notifyActivated = null;
Packit f0b94e
      this._activated = null;
Packit f0b94e
    }
Packit f0b94e
    this._state = aNewState;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  async _changeStateOfflineEvent(offline, calledFromConnEnabledEvent) {
Packit f0b94e
    console.debug("changeStateOfflineEvent()", offline);
Packit f0b94e
Packit f0b94e
    if (this._state < PUSH_SERVICE_ACTIVE_OFFLINE &&
Packit f0b94e
        this._state != PUSH_SERVICE_ACTIVATING &&
Packit f0b94e
        !calledFromConnEnabledEvent) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    if (offline) {
Packit f0b94e
      if (this._state == PUSH_SERVICE_RUNNING) {
Packit f0b94e
        this._service.disconnect();
Packit f0b94e
      }
Packit f0b94e
      this._setState(PUSH_SERVICE_ACTIVE_OFFLINE);
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    if (this._state == PUSH_SERVICE_RUNNING) {
Packit f0b94e
      // PushService was not in the offline state, but got notification to
Packit f0b94e
      // go online (a offline notification has not been sent).
Packit f0b94e
      // Disconnect first.
Packit f0b94e
      this._service.disconnect();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    let records = await this.getAllUnexpired();
Packit f0b94e
Packit f0b94e
    this._setState(PUSH_SERVICE_RUNNING);
Packit f0b94e
Packit f0b94e
    if (records.length > 0 || prefs.get("alwaysConnect")) {
Packit f0b94e
      // Connect if we have existing subscriptions, or if the always-on pref
Packit f0b94e
      // is set.
Packit f0b94e
      this._service.connect(records);
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _changeStateConnectionEnabledEvent: function(enabled) {
Packit f0b94e
    console.debug("changeStateConnectionEnabledEvent()", enabled);
Packit f0b94e
Packit f0b94e
    if (this._state < PUSH_SERVICE_CONNECTION_DISABLE &&
Packit f0b94e
        this._state != PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      return Promise.resolve();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    if (enabled) {
Packit f0b94e
      return this._changeStateOfflineEvent(Services.io.offline, true);
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    if (this._state == PUSH_SERVICE_RUNNING) {
Packit f0b94e
      this._service.disconnect();
Packit f0b94e
    }
Packit f0b94e
    this._setState(PUSH_SERVICE_CONNECTION_DISABLE);
Packit f0b94e
    return Promise.resolve();
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  // Used for testing.
Packit f0b94e
  changeTestServer(url, options = {}) {
Packit f0b94e
    console.debug("changeTestServer()");
Packit f0b94e
Packit f0b94e
    return this._stateChangeProcessEnqueue(_ => {
Packit f0b94e
      if (this._state < PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
        console.debug("changeTestServer: PushService not activated?");
Packit f0b94e
        return Promise.resolve();
Packit f0b94e
      }
Packit f0b94e
Packit f0b94e
      return this._changeServerURL(url, CHANGING_SERVICE_EVENT, options);
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  observe: function observe(aSubject, aTopic, aData) {
Packit f0b94e
    switch (aTopic) {
Packit f0b94e
      /*
Packit f0b94e
       * We need to call uninit() on shutdown to clean up things that modules
Packit f0b94e
       * aren't very good at automatically cleaning up, so we don't get shutdown
Packit f0b94e
       * leaks on browser shutdown.
Packit f0b94e
       */
Packit f0b94e
      case "quit-application":
Packit f0b94e
        this.uninit();
Packit f0b94e
        break;
Packit f0b94e
      case "network:offline-status-changed":
Packit f0b94e
        this._stateChangeProcessEnqueue(_ =>
Packit f0b94e
          this._changeStateOfflineEvent(aData === "offline", false)
Packit f0b94e
        );
Packit f0b94e
        break;
Packit f0b94e
Packit f0b94e
      case "nsPref:changed":
Packit f0b94e
        if (aData == "dom.push.serverURL") {
Packit f0b94e
          console.debug("observe: dom.push.serverURL changed for websocket",
Packit f0b94e
                prefs.get("serverURL"));
Packit f0b94e
          this._stateChangeProcessEnqueue(_ =>
Packit f0b94e
            this._changeServerURL(prefs.get("serverURL"),
Packit f0b94e
                                  CHANGING_SERVICE_EVENT)
Packit f0b94e
          );
Packit f0b94e
Packit f0b94e
        } else if (aData == "dom.push.connection.enabled") {
Packit f0b94e
          this._stateChangeProcessEnqueue(_ =>
Packit f0b94e
            this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"))
Packit f0b94e
          );
Packit f0b94e
        }
Packit f0b94e
        break;
Packit f0b94e
Packit f0b94e
      case "idle-daily":
Packit f0b94e
        this._dropExpiredRegistrations().catch(error => {
Packit f0b94e
          console.error("Failed to drop expired registrations on idle", error);
Packit f0b94e
        });
Packit f0b94e
        break;
Packit f0b94e
Packit f0b94e
      case "perm-changed":
Packit f0b94e
        this._onPermissionChange(aSubject, aData).catch(error => {
Packit f0b94e
          console.error("onPermissionChange: Error updating registrations:",
Packit f0b94e
            error);
Packit f0b94e
        })
Packit f0b94e
        break;
Packit f0b94e
Packit f0b94e
      case "clear-origin-attributes-data":
Packit f0b94e
        this._clearOriginData(aData).catch(error => {
Packit f0b94e
          console.error("clearOriginData: Error clearing origin data:", error);
Packit f0b94e
        });
Packit f0b94e
        break;
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _clearOriginData: function(data) {
Packit f0b94e
    console.log("clearOriginData()");
Packit f0b94e
Packit f0b94e
    if (!data) {
Packit f0b94e
      return Promise.resolve();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    let pattern = JSON.parse(data);
Packit f0b94e
    return this._dropRegistrationsIf(record =>
Packit f0b94e
      record.matchesOriginAttributes(pattern));
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Sends an unregister request to the server in the background. If the
Packit f0b94e
   * service is not connected, this function is a no-op.
Packit f0b94e
   *
Packit f0b94e
   * @param {PushRecord} record The record to unregister.
Packit f0b94e
   * @param {Number} reason An `nsIPushErrorReporter` unsubscribe reason,
Packit f0b94e
   *  indicating why this record was removed.
Packit f0b94e
   */
Packit f0b94e
  _backgroundUnregister(record, reason) {
Packit f0b94e
    console.debug("backgroundUnregister()");
Packit f0b94e
Packit f0b94e
    if (!this._service.isConnected() || !record) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    console.debug("backgroundUnregister: Notifying server", record);
Packit f0b94e
    this._sendUnregister(record, reason).then(() => {
Packit f0b94e
      gPushNotifier.notifySubscriptionModified(record.scope, record.principal);
Packit f0b94e
    }).catch(e => {
Packit f0b94e
      console.error("backgroundUnregister: Error notifying server", e);
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _findService: function(serverURL) {
Packit f0b94e
    console.debug("findService()");
Packit f0b94e
Packit f0b94e
    let uri;
Packit f0b94e
    let service;
Packit f0b94e
Packit f0b94e
    if (!serverURL) {
Packit f0b94e
      console.warn("findService: No dom.push.serverURL found");
Packit f0b94e
      return [];
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    try {
Packit f0b94e
      uri = Services.io.newURI(serverURL);
Packit f0b94e
    } catch (e) {
Packit f0b94e
      console.warn("findService: Error creating valid URI from",
Packit f0b94e
        "dom.push.serverURL", serverURL);
Packit f0b94e
      return [];
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    for (let connProtocol of CONNECTION_PROTOCOLS) {
Packit f0b94e
      if (connProtocol.validServerURI(uri)) {
Packit f0b94e
        service = connProtocol;
Packit f0b94e
        break;
Packit f0b94e
      }
Packit f0b94e
    }
Packit f0b94e
    return [service, uri];
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _changeServerURL: function(serverURI, event, options = {}) {
Packit f0b94e
    console.debug("changeServerURL()");
Packit f0b94e
Packit f0b94e
    switch(event) {
Packit f0b94e
      case UNINIT_EVENT:
Packit f0b94e
        return this._stopService(event);
Packit f0b94e
Packit f0b94e
      case STARTING_SERVICE_EVENT:
Packit f0b94e
      {
Packit f0b94e
        let [service, uri] = this._findService(serverURI);
Packit f0b94e
        if (!service) {
Packit f0b94e
          this._setState(PUSH_SERVICE_INIT);
Packit f0b94e
          return Promise.resolve();
Packit f0b94e
        }
Packit f0b94e
        return this._startService(service, uri, options)
Packit f0b94e
          .then(_ => this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"))
Packit f0b94e
          );
Packit f0b94e
      }
Packit f0b94e
      case CHANGING_SERVICE_EVENT:
Packit f0b94e
        let [service, uri] = this._findService(serverURI);
Packit f0b94e
        if (service) {
Packit f0b94e
          if (this._state == PUSH_SERVICE_INIT) {
Packit f0b94e
            this._setState(PUSH_SERVICE_ACTIVATING);
Packit f0b94e
            // The service has not been running - start it.
Packit f0b94e
            return this._startService(service, uri, options)
Packit f0b94e
              .then(_ => this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"))
Packit f0b94e
              );
Packit f0b94e
Packit f0b94e
          } else {
Packit f0b94e
            this._setState(PUSH_SERVICE_ACTIVATING);
Packit f0b94e
            // If we already had running service - stop service, start the new
Packit f0b94e
            // one and check connection.enabled and offline state(offline state
Packit f0b94e
            // check is called in changeStateConnectionEnabledEvent function)
Packit f0b94e
            return this._stopService(CHANGING_SERVICE_EVENT)
Packit f0b94e
              .then(_ =>
Packit f0b94e
                 this._startService(service, uri, options)
Packit f0b94e
              )
Packit f0b94e
              .then(_ => this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"))
Packit f0b94e
              );
Packit f0b94e
Packit f0b94e
          }
Packit f0b94e
        } else {
Packit f0b94e
          if (this._state == PUSH_SERVICE_INIT) {
Packit f0b94e
            return Promise.resolve();
Packit f0b94e
Packit f0b94e
          } else {
Packit f0b94e
            // The new serverUri is empty or misconfigured - stop service.
Packit f0b94e
            this._setState(PUSH_SERVICE_INIT);
Packit f0b94e
            return this._stopService(STOPPING_SERVICE_EVENT);
Packit f0b94e
          }
Packit f0b94e
        }
Packit f0b94e
      default:
Packit f0b94e
        console.error("Unexpected event in _changeServerURL", event);
Packit f0b94e
        return Promise.reject(new Error(`Unexpected event ${event}`));
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * PushService initialization is divided into 4 parts:
Packit f0b94e
   * init() - start listening for quit-application and serverURL changes.
Packit f0b94e
   *          state is change to PUSH_SERVICE_INIT
Packit f0b94e
   * startService() - if serverURL is present this function is called. It starts
Packit f0b94e
   *                  listening for broadcasted messages, starts db and
Packit f0b94e
   *                  PushService connection (WebSocket).
Packit f0b94e
   *                  state is change to PUSH_SERVICE_ACTIVATING.
Packit f0b94e
   * startObservers() - start other observers.
Packit f0b94e
   * changeStateConnectionEnabledEvent  - checks prefs and offline state.
Packit f0b94e
   *                                      It changes state to:
Packit f0b94e
   *                                        PUSH_SERVICE_RUNNING,
Packit f0b94e
   *                                        PUSH_SERVICE_ACTIVE_OFFLINE or
Packit f0b94e
   *                                        PUSH_SERVICE_CONNECTION_DISABLE.
Packit f0b94e
   */
Packit f0b94e
  init: function(options = {}) {
Packit f0b94e
    console.debug("init()");
Packit f0b94e
Packit f0b94e
    if (this._state > PUSH_SERVICE_UNINIT) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    this._setState(PUSH_SERVICE_ACTIVATING);
Packit f0b94e
Packit f0b94e
    prefs.observe("serverURL", this);
Packit f0b94e
    Services.obs.addObserver(this, "quit-application");
Packit f0b94e
Packit f0b94e
    if (options.serverURI) {
Packit f0b94e
      // this is use for xpcshell test.
Packit f0b94e
Packit f0b94e
      this._stateChangeProcessEnqueue(_ =>
Packit f0b94e
        this._changeServerURL(options.serverURI, STARTING_SERVICE_EVENT, options));
Packit f0b94e
Packit f0b94e
    } else {
Packit f0b94e
      // This is only used for testing. Different tests require connecting to
Packit f0b94e
      // slightly different URLs.
Packit f0b94e
      this._stateChangeProcessEnqueue(_ =>
Packit f0b94e
        this._changeServerURL(prefs.get("serverURL"), STARTING_SERVICE_EVENT));
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _startObservers: function() {
Packit f0b94e
    console.debug("startObservers()");
Packit f0b94e
Packit f0b94e
    if (this._state != PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    Services.obs.addObserver(this, "clear-origin-attributes-data");
Packit f0b94e
Packit f0b94e
    // The offline-status-changed event is used to know
Packit f0b94e
    // when to (dis)connect. It may not fire if the underlying OS changes
Packit f0b94e
    // networks; in such a case we rely on timeout.
Packit f0b94e
    Services.obs.addObserver(this, "network:offline-status-changed");
Packit f0b94e
Packit f0b94e
    // Used to monitor if the user wishes to disable Push.
Packit f0b94e
    prefs.observe("connection.enabled", this);
Packit f0b94e
Packit f0b94e
    // Prunes expired registrations and notifies dormant service workers.
Packit f0b94e
    Services.obs.addObserver(this, "idle-daily");
Packit f0b94e
Packit f0b94e
    // Prunes registrations for sites for which the user revokes push
Packit f0b94e
    // permissions.
Packit f0b94e
    Services.obs.addObserver(this, "perm-changed");
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _startService(service, serverURI, options) {
Packit f0b94e
    console.debug("startService()");
Packit f0b94e
Packit f0b94e
    if (this._state != PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      return Promise.reject();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    this._service = service;
Packit f0b94e
Packit f0b94e
    this._db = options.db;
Packit f0b94e
    if (!this._db) {
Packit f0b94e
      this._db = this._service.newPushDB();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    return this._service.init(options, this, serverURI)
Packit f0b94e
      .then(() => {
Packit f0b94e
        this._startObservers();
Packit f0b94e
        return this._dropExpiredRegistrations();
Packit f0b94e
      });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * PushService uninitialization is divided into 3 parts:
Packit f0b94e
   * stopObservers() - stot observers started in startObservers.
Packit f0b94e
   * stopService() - It stops listening for broadcasted messages, stops db and
Packit f0b94e
   *                 PushService connection (WebSocket).
Packit f0b94e
   *                 state is changed to PUSH_SERVICE_INIT.
Packit f0b94e
   * uninit() - stop listening for quit-application and serverURL changes.
Packit f0b94e
   *            state is change to PUSH_SERVICE_UNINIT
Packit f0b94e
   */
Packit f0b94e
  _stopService: function(event) {
Packit f0b94e
    console.debug("stopService()");
Packit f0b94e
Packit f0b94e
    if (this._state < PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      return Promise.resolve();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    this._stopObservers();
Packit f0b94e
Packit f0b94e
    this._service.disconnect();
Packit f0b94e
    this._service.uninit();
Packit f0b94e
    this._service = null;
Packit f0b94e
Packit f0b94e
    this._updateQuotaTimeouts.forEach((timeoutID) => clearTimeout(timeoutID));
Packit f0b94e
    this._updateQuotaTimeouts.clear();
Packit f0b94e
Packit f0b94e
    if (!this._db) {
Packit f0b94e
      return Promise.resolve();
Packit f0b94e
    }
Packit f0b94e
    if (event == UNINIT_EVENT) {
Packit f0b94e
      // If it is uninitialized just close db.
Packit f0b94e
      this._db.close();
Packit f0b94e
      this._db = null;
Packit f0b94e
      return Promise.resolve();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    return this.dropUnexpiredRegistrations()
Packit f0b94e
       .then(_ => {
Packit f0b94e
         this._db.close();
Packit f0b94e
         this._db = null;
Packit f0b94e
       }, err => {
Packit f0b94e
         this._db.close();
Packit f0b94e
         this._db = null;
Packit f0b94e
       });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _stopObservers: function() {
Packit f0b94e
    console.debug("stopObservers()");
Packit f0b94e
Packit f0b94e
    if (this._state < PUSH_SERVICE_ACTIVATING) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    prefs.ignore("connection.enabled", this);
Packit f0b94e
Packit f0b94e
    Services.obs.removeObserver(this, "network:offline-status-changed");
Packit f0b94e
    Services.obs.removeObserver(this, "clear-origin-attributes-data");
Packit f0b94e
    Services.obs.removeObserver(this, "idle-daily");
Packit f0b94e
    Services.obs.removeObserver(this, "perm-changed");
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _shutdownService() {
Packit f0b94e
    let promiseChangeURL = this._changeServerURL("", UNINIT_EVENT);
Packit f0b94e
    this._setState(PUSH_SERVICE_UNINIT);
Packit f0b94e
    console.debug("shutdownService: shutdown complete!");
Packit f0b94e
    return promiseChangeURL;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  uninit: function() {
Packit f0b94e
    console.debug("uninit()");
Packit f0b94e
Packit f0b94e
    if (this._state == PUSH_SERVICE_UNINIT) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    prefs.ignore("serverURL", this);
Packit f0b94e
    Services.obs.removeObserver(this, "quit-application");
Packit f0b94e
Packit f0b94e
    this._stateChangeProcessEnqueue(_ => this._shutdownService());
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Drops all active registrations and notifies the associated service
Packit f0b94e
   * workers. This function is called when the user switches Push servers,
Packit f0b94e
   * or when the server invalidates all existing registrations.
Packit f0b94e
   *
Packit f0b94e
   * We ignore expired registrations because they're already handled in other
Packit f0b94e
   * code paths. Registrations that expired after exceeding their quotas are
Packit f0b94e
   * evicted at startup, or on the next `idle-daily` event. Registrations that
Packit f0b94e
   * expired because the user revoked the notification permission are evicted
Packit f0b94e
   * once the permission is reinstated.
Packit f0b94e
   */
Packit f0b94e
  dropUnexpiredRegistrations: function() {
Packit f0b94e
    return this._db.clearIf(record => {
Packit f0b94e
      if (record.isExpired()) {
Packit f0b94e
        return false;
Packit f0b94e
      }
Packit f0b94e
      this._notifySubscriptionChangeObservers(record);
Packit f0b94e
      return true;
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _notifySubscriptionChangeObservers: function(record) {
Packit f0b94e
    if (!record) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    gPushNotifier.notifySubscriptionChange(record.scope, record.principal);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Drops a registration and notifies the associated service worker. If the
Packit f0b94e
   * registration does not exist, this function is a no-op.
Packit f0b94e
   *
Packit f0b94e
   * @param {String} keyID The registration ID to remove.
Packit f0b94e
   * @returns {Promise} Resolves once the worker has been notified.
Packit f0b94e
   */
Packit f0b94e
  dropRegistrationAndNotifyApp: function(aKeyID) {
Packit f0b94e
    return this._db.delete(aKeyID)
Packit f0b94e
      .then(record => this._notifySubscriptionChangeObservers(record));
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Replaces an existing registration and notifies the associated service
Packit f0b94e
   * worker.
Packit f0b94e
   *
Packit f0b94e
   * @param {String} aOldKey The registration ID to replace.
Packit f0b94e
   * @param {PushRecord} aNewRecord The new record.
Packit f0b94e
   * @returns {Promise} Resolves once the worker has been notified.
Packit f0b94e
   */
Packit f0b94e
  updateRegistrationAndNotifyApp: function(aOldKey, aNewRecord) {
Packit f0b94e
    return this.updateRecordAndNotifyApp(aOldKey, _ => aNewRecord);
Packit f0b94e
  },
Packit f0b94e
  /**
Packit f0b94e
   * Updates a registration and notifies the associated service worker.
Packit f0b94e
   *
Packit f0b94e
   * @param {String} keyID The registration ID to update.
Packit f0b94e
   * @param {Function} updateFunc Returns the updated record.
Packit f0b94e
   * @returns {Promise} Resolves with the updated record once the worker
Packit f0b94e
   *  has been notified.
Packit f0b94e
   */
Packit f0b94e
  updateRecordAndNotifyApp: function(aKeyID, aUpdateFunc) {
Packit f0b94e
    return this._db.update(aKeyID, aUpdateFunc)
Packit f0b94e
      .then(record => {
Packit f0b94e
        this._notifySubscriptionChangeObservers(record);
Packit f0b94e
        return record;
Packit f0b94e
      });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  ensureCrypto: function(record) {
Packit f0b94e
    if (record.hasAuthenticationSecret() &&
Packit f0b94e
        record.p256dhPublicKey &&
Packit f0b94e
        record.p256dhPrivateKey) {
Packit f0b94e
      return Promise.resolve(record);
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    let keygen = Promise.resolve([]);
Packit f0b94e
    if (!record.p256dhPublicKey || !record.p256dhPrivateKey) {
Packit f0b94e
      keygen = PushCrypto.generateKeys();
Packit f0b94e
    }
Packit f0b94e
    // We do not have a encryption key. so we need to generate it. This
Packit f0b94e
    // is only going to happen on db upgrade from version 4 to higher.
Packit f0b94e
    return keygen
Packit f0b94e
      .then(([pubKey, privKey]) => {
Packit f0b94e
        return this.updateRecordAndNotifyApp(record.keyID, record => {
Packit f0b94e
          if (!record.p256dhPublicKey || !record.p256dhPrivateKey) {
Packit f0b94e
            record.p256dhPublicKey = pubKey;
Packit f0b94e
            record.p256dhPrivateKey = privKey;
Packit f0b94e
          }
Packit f0b94e
          if (!record.hasAuthenticationSecret()) {
Packit f0b94e
            record.authenticationSecret = PushCrypto.generateAuthenticationSecret();
Packit f0b94e
          }
Packit f0b94e
          return record;
Packit f0b94e
        });
Packit f0b94e
      }, error => {
Packit f0b94e
        return this.dropRegistrationAndNotifyApp(record.keyID).then(
Packit f0b94e
          () => Promise.reject(error));
Packit f0b94e
      });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Dispatches an incoming message to a service worker, recalculating the
Packit f0b94e
   * quota for the associated push registration. If the quota is exceeded,
Packit f0b94e
   * the registration and message will be dropped, and the worker will not
Packit f0b94e
   * be notified.
Packit f0b94e
   *
Packit f0b94e
   * @param {String} keyID The push registration ID.
Packit f0b94e
   * @param {String} messageID The message ID, used to report service worker
Packit f0b94e
   *  delivery failures. For Web Push messages, this is the version. If empty,
Packit f0b94e
   *  failures will not be reported.
Packit f0b94e
   * @param {Object} headers The encryption headers.
Packit f0b94e
   * @param {ArrayBuffer|Uint8Array} data The encrypted message data.
Packit f0b94e
   * @param {Function} updateFunc A function that receives the existing
Packit f0b94e
   *  registration record as its argument, and returns a new record. If the
Packit f0b94e
   *  function returns `null` or `undefined`, the record will not be updated.
Packit f0b94e
   *  `PushServiceWebSocket` uses this to drop incoming updates with older
Packit f0b94e
   *  versions.
Packit f0b94e
   * @returns {Promise} Resolves with an `nsIPushErrorReporter` ack status
Packit f0b94e
   *  code, indicating whether the message was delivered successfully.
Packit f0b94e
   */
Packit f0b94e
  receivedPushMessage(keyID, messageID, headers, data, updateFunc) {
Packit f0b94e
    console.debug("receivedPushMessage()");
Packit f0b94e
Packit f0b94e
    return this._updateRecordAfterPush(keyID, updateFunc).then(record => {
Packit f0b94e
      if (record.quotaApplies()) {
Packit f0b94e
        // Update quota after the delay, at which point
Packit f0b94e
        // we check for visible notifications.
Packit f0b94e
        let timeoutID = setTimeout(_ =>
Packit f0b94e
          {
Packit f0b94e
            this._updateQuota(keyID);
Packit f0b94e
            if (!this._updateQuotaTimeouts.delete(timeoutID)) {
Packit f0b94e
              console.debug("receivedPushMessage: quota update timeout missing?");
Packit f0b94e
            }
Packit f0b94e
          }, prefs.get("quotaUpdateDelay"));
Packit f0b94e
        this._updateQuotaTimeouts.add(timeoutID);
Packit f0b94e
      }
Packit f0b94e
      return this._decryptAndNotifyApp(record, messageID, headers, data);
Packit f0b94e
    }).catch(error => {
Packit f0b94e
      console.error("receivedPushMessage: Error notifying app", error);
Packit f0b94e
      return Ci.nsIPushErrorReporter.ACK_NOT_DELIVERED;
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Updates a registration record after receiving a push message.
Packit f0b94e
   *
Packit f0b94e
   * @param {String} keyID The push registration ID.
Packit f0b94e
   * @param {Function} updateFunc The function passed to `receivedPushMessage`.
Packit f0b94e
   * @returns {Promise} Resolves with the updated record, or rejects if the
Packit f0b94e
   *  record was not updated.
Packit f0b94e
   */
Packit f0b94e
  _updateRecordAfterPush(keyID, updateFunc) {
Packit f0b94e
    return this.getByKeyID(keyID).then(record => {
Packit f0b94e
      if (!record) {
Packit f0b94e
        throw new Error("No record for key ID " + keyID);
Packit f0b94e
      }
Packit f0b94e
      return record.getLastVisit().then(lastVisit => {
Packit f0b94e
        // As a special case, don't notify the service worker if the user
Packit f0b94e
        // cleared their history.
Packit f0b94e
        if (!isFinite(lastVisit)) {
Packit f0b94e
          throw new Error("Ignoring message sent to unvisited origin");
Packit f0b94e
        }
Packit f0b94e
        return lastVisit;
Packit f0b94e
      }).then(lastVisit => {
Packit f0b94e
        // Update the record, resetting the quota if the user has visited the
Packit f0b94e
        // site since the last push.
Packit f0b94e
        return this._db.update(keyID, record => {
Packit f0b94e
          let newRecord = updateFunc(record);
Packit f0b94e
          if (!newRecord) {
Packit f0b94e
            return null;
Packit f0b94e
          }
Packit f0b94e
          // Because `unregister` is advisory only, we can still receive messages
Packit f0b94e
          // for stale Simple Push registrations from the server. To work around
Packit f0b94e
          // this, we check if the record has expired before *and* after updating
Packit f0b94e
          // the quota.
Packit f0b94e
          if (newRecord.isExpired()) {
Packit f0b94e
            return null;
Packit f0b94e
          }
Packit f0b94e
          newRecord.receivedPush(lastVisit);
Packit f0b94e
          return newRecord;
Packit f0b94e
        });
Packit f0b94e
      });
Packit f0b94e
    }).then(record => {
Packit f0b94e
      gPushNotifier.notifySubscriptionModified(record.scope,
Packit f0b94e
                                               record.principal);
Packit f0b94e
      return record;
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Decrypts an incoming message and notifies the associated service worker.
Packit f0b94e
   *
Packit f0b94e
   * @param {PushRecord} record The receiving registration.
Packit f0b94e
   * @param {String} messageID The message ID.
Packit f0b94e
   * @param {Object} headers The encryption headers.
Packit f0b94e
   * @param {ArrayBuffer|Uint8Array} data The encrypted message data.
Packit f0b94e
   * @returns {Promise} Resolves with an ack status code.
Packit f0b94e
   */
Packit f0b94e
  _decryptAndNotifyApp(record, messageID, headers, data) {
Packit f0b94e
    return PushCrypto.decrypt(record.p256dhPrivateKey, record.p256dhPublicKey,
Packit f0b94e
                              record.authenticationSecret, headers, data)
Packit f0b94e
      .then(
Packit f0b94e
        message => this._notifyApp(record, messageID, message),
Packit f0b94e
        error => {
Packit f0b94e
          console.warn("decryptAndNotifyApp: Error decrypting message",
Packit f0b94e
            record.scope, messageID, error);
Packit f0b94e
Packit f0b94e
          let message = error.format(record.scope);
Packit f0b94e
          gPushNotifier.notifyError(record.scope, record.principal, message,
Packit f0b94e
                                    Ci.nsIScriptError.errorFlag);
Packit f0b94e
          return Ci.nsIPushErrorReporter.ACK_DECRYPTION_ERROR;
Packit f0b94e
        });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _updateQuota: function(keyID) {
Packit f0b94e
    console.debug("updateQuota()");
Packit f0b94e
Packit f0b94e
    this._db.update(keyID, record => {
Packit f0b94e
      // Record may have expired from an earlier quota update.
Packit f0b94e
      if (record.isExpired()) {
Packit f0b94e
        console.debug(
Packit f0b94e
          "updateQuota: Trying to update quota for expired record", record);
Packit f0b94e
        return null;
Packit f0b94e
      }
Packit f0b94e
      // If there are visible notifications, don't apply the quota penalty
Packit f0b94e
      // for the message.
Packit f0b94e
      if (record.uri && !this._visibleNotifications.has(record.uri.prePath)) {
Packit f0b94e
        record.reduceQuota();
Packit f0b94e
      }
Packit f0b94e
      return record;
Packit f0b94e
    }).then(record => {
Packit f0b94e
      if (record.isExpired()) {
Packit f0b94e
        // Drop the registration in the background. If the user returns to the
Packit f0b94e
        // site, the service worker will be notified on the next `idle-daily`
Packit f0b94e
        // event.
Packit f0b94e
        this._backgroundUnregister(record,
Packit f0b94e
          Ci.nsIPushErrorReporter.UNSUBSCRIBE_QUOTA_EXCEEDED);
Packit f0b94e
      } else {
Packit f0b94e
        gPushNotifier.notifySubscriptionModified(record.scope,
Packit f0b94e
                                                 record.principal);
Packit f0b94e
      }
Packit f0b94e
      if (this._updateQuotaTestCallback) {
Packit f0b94e
        // Callback so that test may be notified when the quota update is complete.
Packit f0b94e
        this._updateQuotaTestCallback();
Packit f0b94e
      }
Packit f0b94e
    }).catch(error => {
Packit f0b94e
      console.debug("updateQuota: Error while trying to update quota", error);
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  notificationForOriginShown(origin) {
Packit f0b94e
    console.debug("notificationForOriginShown()", origin);
Packit f0b94e
    let count;
Packit f0b94e
    if (this._visibleNotifications.has(origin)) {
Packit f0b94e
      count = this._visibleNotifications.get(origin);
Packit f0b94e
    } else {
Packit f0b94e
      count = 0;
Packit f0b94e
    }
Packit f0b94e
    this._visibleNotifications.set(origin, count + 1);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  notificationForOriginClosed(origin) {
Packit f0b94e
    console.debug("notificationForOriginClosed()", origin);
Packit f0b94e
    let count;
Packit f0b94e
    if (this._visibleNotifications.has(origin)) {
Packit f0b94e
      count = this._visibleNotifications.get(origin);
Packit f0b94e
    } else {
Packit f0b94e
      console.debug("notificationForOriginClosed: closing notification that has not been shown?");
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    if (count > 1) {
Packit f0b94e
      this._visibleNotifications.set(origin, count - 1);
Packit f0b94e
    } else {
Packit f0b94e
      this._visibleNotifications.delete(origin);
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  reportDeliveryError(messageID, reason) {
Packit f0b94e
    console.debug("reportDeliveryError()", messageID, reason);
Packit f0b94e
    if (this._state == PUSH_SERVICE_RUNNING &&
Packit f0b94e
        this._service.isConnected()) {
Packit f0b94e
Packit f0b94e
      // Only report errors if we're initialized and connected.
Packit f0b94e
      this._service.reportDeliveryError(messageID, reason);
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _notifyApp(aPushRecord, messageID, message) {
Packit f0b94e
    if (!aPushRecord || !aPushRecord.scope ||
Packit f0b94e
        aPushRecord.originAttributes === undefined) {
Packit f0b94e
      console.error("notifyApp: Invalid record", aPushRecord);
Packit f0b94e
      return Ci.nsIPushErrorReporter.ACK_NOT_DELIVERED;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    console.debug("notifyApp()", aPushRecord.scope);
Packit f0b94e
Packit f0b94e
    // If permission has been revoked, trash the message.
Packit f0b94e
    if (!aPushRecord.hasPermission()) {
Packit f0b94e
      console.warn("notifyApp: Missing push permission", aPushRecord);
Packit f0b94e
      return Ci.nsIPushErrorReporter.ACK_NOT_DELIVERED;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    let payload = ArrayBuffer.isView(message) ?
Packit f0b94e
                  new Uint8Array(message.buffer) : message;
Packit f0b94e
Packit f0b94e
    if (aPushRecord.quotaApplies()) {
Packit f0b94e
      // Don't record telemetry for chrome push messages.
Packit f0b94e
      Services.telemetry.getHistogramById("PUSH_API_NOTIFY").add();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    if (payload) {
Packit f0b94e
      gPushNotifier.notifyPushWithData(aPushRecord.scope,
Packit f0b94e
                                       aPushRecord.principal,
Packit f0b94e
                                       messageID, payload.length, payload);
Packit f0b94e
    } else {
Packit f0b94e
      gPushNotifier.notifyPush(aPushRecord.scope, aPushRecord.principal,
Packit f0b94e
                               messageID);
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    return Ci.nsIPushErrorReporter.ACK_DELIVERED;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  getByKeyID: function(aKeyID) {
Packit f0b94e
    return this._db.getByKeyID(aKeyID);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  getAllUnexpired: function() {
Packit f0b94e
    return this._db.getAllUnexpired();
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _sendRequest(action, ...params) {
Packit f0b94e
    if (this._state == PUSH_SERVICE_CONNECTION_DISABLE) {
Packit f0b94e
      return Promise.reject(new Error("Push service disabled"));
Packit f0b94e
    }
Packit f0b94e
    if (this._state == PUSH_SERVICE_ACTIVE_OFFLINE) {
Packit f0b94e
      return Promise.reject(new Error("Push service offline"));
Packit f0b94e
    }
Packit f0b94e
    // Ensure the backend is ready. `getByPageRecord` already checks this, but
Packit f0b94e
    // we need to check again here in case the service was restarted in the
Packit f0b94e
    // meantime.
Packit f0b94e
    return this._checkActivated().then(_ => {
Packit f0b94e
      switch (action) {
Packit f0b94e
        case "register":
Packit f0b94e
          return this._service.register(...params);
Packit f0b94e
        case "unregister":
Packit f0b94e
          return this._service.unregister(...params);
Packit f0b94e
      }
Packit f0b94e
      return Promise.reject(new Error("Unknown request type: " + action));
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Called on message from the child process. aPageRecord is an object sent by
Packit f0b94e
   * the push manager, identifying the sending page and other fields.
Packit f0b94e
   */
Packit f0b94e
  _registerWithServer: function(aPageRecord) {
Packit f0b94e
    console.debug("registerWithServer()", aPageRecord);
Packit f0b94e
Packit f0b94e
    return this._sendRequest("register", aPageRecord)
Packit f0b94e
      .then(record => this._onRegisterSuccess(record),
Packit f0b94e
            err => this._onRegisterError(err))
Packit f0b94e
      .then(record => {
Packit f0b94e
        this._deletePendingRequest(aPageRecord);
Packit f0b94e
        gPushNotifier.notifySubscriptionModified(record.scope,
Packit f0b94e
                                                 record.principal);
Packit f0b94e
        return record.toSubscription();
Packit f0b94e
      }, err => {
Packit f0b94e
        this._deletePendingRequest(aPageRecord);
Packit f0b94e
        throw err;
Packit f0b94e
     });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _sendUnregister(aRecord, aReason) {
Packit f0b94e
    return this._sendRequest("unregister", aRecord, aReason);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
Packit f0b94e
   * from _service.request, causing the promise to be rejected instead.
Packit f0b94e
   */
Packit f0b94e
  _onRegisterSuccess: function(aRecord) {
Packit f0b94e
    console.debug("_onRegisterSuccess()");
Packit f0b94e
Packit f0b94e
    return this._db.put(aRecord)
Packit f0b94e
      .catch(error => {
Packit f0b94e
        // Unable to save. Destroy the subscription in the background.
Packit f0b94e
        this._backgroundUnregister(aRecord,
Packit f0b94e
                                   Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL);
Packit f0b94e
        throw error;
Packit f0b94e
      });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Exceptions thrown in _onRegisterError are caught by the promise obtained
Packit f0b94e
   * from _service.request, causing the promise to be rejected instead.
Packit f0b94e
   */
Packit f0b94e
  _onRegisterError: function(reply) {
Packit f0b94e
    console.debug("_onRegisterError()");
Packit f0b94e
Packit f0b94e
    if (!reply.error) {
Packit f0b94e
      console.warn("onRegisterError: Called without valid error message!",
Packit f0b94e
        reply);
Packit f0b94e
      throw new Error("Registration error");
Packit f0b94e
    }
Packit f0b94e
    throw reply.error;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  notificationsCleared() {
Packit f0b94e
    this._visibleNotifications.clear();
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _getByPageRecord(pageRecord) {
Packit f0b94e
    return this._checkActivated().then(_ =>
Packit f0b94e
      this._db.getByIdentifiers(pageRecord)
Packit f0b94e
    );
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  register: function(aPageRecord) {
Packit f0b94e
    console.debug("register()", aPageRecord);
Packit f0b94e
Packit f0b94e
    let keyPromise;
Packit f0b94e
    if (aPageRecord.appServerKey) {
Packit f0b94e
      let keyView = new Uint8Array(aPageRecord.appServerKey);
Packit f0b94e
      keyPromise = PushCrypto.validateAppServerKey(keyView)
Packit f0b94e
        .catch(error => {
Packit f0b94e
          // Normalize Web Crypto exceptions. `nsIPushService` will forward the
Packit f0b94e
          // error result to the DOM API implementation in `PushManager.cpp` or
Packit f0b94e
          // `Push.js`, which will convert it to the correct `DOMException`.
Packit f0b94e
          throw errorWithResult("Invalid app server key",
Packit f0b94e
                                Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
Packit f0b94e
        });
Packit f0b94e
    } else {
Packit f0b94e
      keyPromise = Promise.resolve(null);
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    return Promise.all([
Packit f0b94e
      keyPromise,
Packit f0b94e
      this._getByPageRecord(aPageRecord),
Packit f0b94e
    ]).then(([appServerKey, record]) => {
Packit f0b94e
      aPageRecord.appServerKey = appServerKey;
Packit f0b94e
      if (!record) {
Packit f0b94e
        return this._lookupOrPutPendingRequest(aPageRecord);
Packit f0b94e
      }
Packit f0b94e
      if (!record.matchesAppServerKey(appServerKey)) {
Packit f0b94e
        throw errorWithResult("Mismatched app server key",
Packit f0b94e
                              Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR);
Packit f0b94e
      }
Packit f0b94e
      if (record.isExpired()) {
Packit f0b94e
        return record.quotaChanged().then(isChanged => {
Packit f0b94e
          if (isChanged) {
Packit f0b94e
            // If the user revisited the site, drop the expired push
Packit f0b94e
            // registration and re-register.
Packit f0b94e
            return this.dropRegistrationAndNotifyApp(record.keyID);
Packit f0b94e
          }
Packit f0b94e
          throw new Error("Push subscription expired");
Packit f0b94e
        }).then(_ => this._lookupOrPutPendingRequest(aPageRecord));
Packit f0b94e
      }
Packit f0b94e
      return record.toSubscription();
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Called on message from the child process.
Packit f0b94e
   *
Packit f0b94e
   * Why is the record being deleted from the local database before the server
Packit f0b94e
   * is told?
Packit f0b94e
   *
Packit f0b94e
   * Unregistration is for the benefit of the app and the AppServer
Packit f0b94e
   * so that the AppServer does not keep pinging a channel the UserAgent isn't
Packit f0b94e
   * watching The important part of the transaction in this case is left to the
Packit f0b94e
   * app, to tell its server of the unregistration.  Even if the request to the
Packit f0b94e
   * PushServer were to fail, it would not affect correctness of the protocol,
Packit f0b94e
   * and the server GC would just clean up the channelID/subscription
Packit f0b94e
   * eventually.  Since the appserver doesn't ping it, no data is lost.
Packit f0b94e
   *
Packit f0b94e
   * If rather we were to unregister at the server and update the database only
Packit f0b94e
   * on success: If the server receives the unregister, and deletes the
Packit f0b94e
   * channelID/subscription, but the response is lost because of network
Packit f0b94e
   * failure, the application is never informed. In addition the application may
Packit f0b94e
   * retry the unregister when it fails due to timeout (websocket) or any other
Packit f0b94e
   * reason at which point the server will say it does not know of this
Packit f0b94e
   * unregistration.  We'll have to make the registration/unregistration phases
Packit f0b94e
   * have retries and attempts to resend messages from the server, and have the
Packit f0b94e
   * client acknowledge. On a server, data is cheap, reliable notification is
Packit f0b94e
   * not.
Packit f0b94e
   */
Packit f0b94e
  unregister: function(aPageRecord) {
Packit f0b94e
    console.debug("unregister()", aPageRecord);
Packit f0b94e
Packit f0b94e
    return this._getByPageRecord(aPageRecord)
Packit f0b94e
      .then(record => {
Packit f0b94e
        if (record === undefined) {
Packit f0b94e
          return false;
Packit f0b94e
        }
Packit f0b94e
Packit f0b94e
        let reason = Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL;
Packit f0b94e
        return Promise.all([
Packit f0b94e
          this._sendUnregister(record, reason),
Packit f0b94e
          this._db.delete(record.keyID).then(record => {
Packit f0b94e
            if (record) {
Packit f0b94e
              gPushNotifier.notifySubscriptionModified(record.scope,
Packit f0b94e
                                                       record.principal);
Packit f0b94e
            }
Packit f0b94e
          }),
Packit f0b94e
        ]).then(([success]) => success);
Packit f0b94e
      });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  clear: function(info) {
Packit f0b94e
    return this._checkActivated()
Packit f0b94e
      .then(_ => {
Packit f0b94e
        return this._dropRegistrationsIf(record =>
Packit f0b94e
          info.domain == "*" ||
Packit f0b94e
          (record.uri && hasRootDomain(record.uri.prePath, info.domain))
Packit f0b94e
        );
Packit f0b94e
      })
Packit f0b94e
      .catch(e => {
Packit f0b94e
        console.warn("clear: Error dropping subscriptions for domain",
Packit f0b94e
          info.domain, e);
Packit f0b94e
        return Promise.resolve();
Packit f0b94e
      });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  registration: function(aPageRecord) {
Packit f0b94e
    console.debug("registration()");
Packit f0b94e
Packit f0b94e
    return this._getByPageRecord(aPageRecord)
Packit f0b94e
      .then(record => {
Packit f0b94e
        if (!record) {
Packit f0b94e
          return null;
Packit f0b94e
        }
Packit f0b94e
        if (record.isExpired()) {
Packit f0b94e
          return record.quotaChanged().then(isChanged => {
Packit f0b94e
            if (isChanged) {
Packit f0b94e
              return this.dropRegistrationAndNotifyApp(record.keyID).then(_ => null);
Packit f0b94e
            }
Packit f0b94e
            return null;
Packit f0b94e
          });
Packit f0b94e
        }
Packit f0b94e
        return record.toSubscription();
Packit f0b94e
      });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _dropExpiredRegistrations: function() {
Packit f0b94e
    console.debug("dropExpiredRegistrations()");
Packit f0b94e
Packit f0b94e
    return this._db.getAllExpired().then(records => {
Packit f0b94e
      return Promise.all(records.map(record =>
Packit f0b94e
        record.quotaChanged().then(isChanged => {
Packit f0b94e
          if (isChanged) {
Packit f0b94e
            // If the user revisited the site, drop the expired push
Packit f0b94e
            // registration and notify the associated service worker.
Packit f0b94e
            return this.dropRegistrationAndNotifyApp(record.keyID);
Packit f0b94e
          }
Packit f0b94e
        }).catch(error => {
Packit f0b94e
          console.error("dropExpiredRegistrations: Error dropping registration",
Packit f0b94e
            record.keyID, error);
Packit f0b94e
        })
Packit f0b94e
      ));
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _onPermissionChange: function(subject, data) {
Packit f0b94e
    console.debug("onPermissionChange()");
Packit f0b94e
Packit f0b94e
    if (data == "cleared") {
Packit f0b94e
      return this._clearPermissions();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    let permission = subject.QueryInterface(Ci.nsIPermission);
Packit f0b94e
    if (permission.type != "desktop-notification") {
Packit f0b94e
      return Promise.resolve();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    return this._updatePermission(permission, data);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _clearPermissions() {
Packit f0b94e
    console.debug("clearPermissions()");
Packit f0b94e
Packit f0b94e
    return this._db.clearIf(record => {
Packit f0b94e
      if (!record.quotaApplies()) {
Packit f0b94e
        // Only drop registrations that are subject to quota.
Packit f0b94e
        return false;
Packit f0b94e
      }
Packit f0b94e
      this._backgroundUnregister(record,
Packit f0b94e
        Ci.nsIPushErrorReporter.UNSUBSCRIBE_PERMISSION_REVOKED);
Packit f0b94e
      return true;
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _updatePermission: function(permission, type) {
Packit f0b94e
    console.debug("updatePermission()");
Packit f0b94e
Packit f0b94e
    let isAllow = permission.capability ==
Packit f0b94e
                  Ci.nsIPermissionManager.ALLOW_ACTION;
Packit f0b94e
    let isChange = type == "added" || type == "changed";
Packit f0b94e
Packit f0b94e
    if (isAllow && isChange) {
Packit f0b94e
      // Permission set to "allow". Drop all expired registrations for this
Packit f0b94e
      // site, notify the associated service workers, and reset the quota
Packit f0b94e
      // for active registrations.
Packit f0b94e
      return this._forEachPrincipal(
Packit f0b94e
        permission.principal,
Packit f0b94e
        (record, cursor) => this._permissionAllowed(record, cursor)
Packit f0b94e
      );
Packit f0b94e
    } else if (isChange || (isAllow && type == "deleted")) {
Packit f0b94e
      // Permission set to "block" or "always ask," or "allow" permission
Packit f0b94e
      // removed. Expire all registrations for this site.
Packit f0b94e
      return this._forEachPrincipal(
Packit f0b94e
        permission.principal,
Packit f0b94e
        (record, cursor) => this._permissionDenied(record, cursor)
Packit f0b94e
      );
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    return Promise.resolve();
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  _forEachPrincipal: function(principal, callback) {
Packit f0b94e
    return this._db.forEachOrigin(
Packit f0b94e
      principal.URI.prePath,
Packit f0b94e
      ChromeUtils.originAttributesToSuffix(principal.originAttributes),
Packit f0b94e
      callback
Packit f0b94e
    );
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * The update function called for each registration record if the push
Packit f0b94e
   * permission is revoked. We only expire the record so we can notify the
Packit f0b94e
   * service worker as soon as the permission is reinstated. If we just
Packit f0b94e
   * deleted the record, the worker wouldn't be notified until the next visit
Packit f0b94e
   * to the site.
Packit f0b94e
   *
Packit f0b94e
   * @param {PushRecord} record The record to expire.
Packit f0b94e
   * @param {IDBCursor} cursor The IndexedDB cursor.
Packit f0b94e
   */
Packit f0b94e
  _permissionDenied: function(record, cursor) {
Packit f0b94e
    console.debug("permissionDenied()");
Packit f0b94e
Packit f0b94e
    if (!record.quotaApplies() || record.isExpired()) {
Packit f0b94e
      // Ignore already-expired records.
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    // Drop the registration in the background.
Packit f0b94e
    this._backgroundUnregister(record,
Packit f0b94e
      Ci.nsIPushErrorReporter.UNSUBSCRIBE_PERMISSION_REVOKED);
Packit f0b94e
    record.setQuota(0);
Packit f0b94e
    cursor.update(record);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * The update function called for each registration record if the push
Packit f0b94e
   * permission is granted. If the record has expired, it will be dropped;
Packit f0b94e
   * otherwise, its quota will be reset to the default value.
Packit f0b94e
   *
Packit f0b94e
   * @param {PushRecord} record The record to update.
Packit f0b94e
   * @param {IDBCursor} cursor The IndexedDB cursor.
Packit f0b94e
   */
Packit f0b94e
  _permissionAllowed(record, cursor) {
Packit f0b94e
    console.debug("permissionAllowed()");
Packit f0b94e
Packit f0b94e
    if (!record.quotaApplies()) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    if (record.isExpired()) {
Packit f0b94e
      // If the registration has expired, drop and notify the worker
Packit f0b94e
      // unconditionally.
Packit f0b94e
      this._notifySubscriptionChangeObservers(record);
Packit f0b94e
      cursor.delete();
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    record.resetQuota();
Packit f0b94e
    cursor.update(record);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Drops all matching registrations from the database. Notifies the
Packit f0b94e
   * associated service workers if permission is granted, and removes
Packit f0b94e
   * unexpired registrations from the server.
Packit f0b94e
   *
Packit f0b94e
   * @param {Function} predicate A function called for each record.
Packit f0b94e
   * @returns {Promise} Resolves once the registrations have been dropped.
Packit f0b94e
   */
Packit f0b94e
  _dropRegistrationsIf(predicate) {
Packit f0b94e
    return this._db.clearIf(record => {
Packit f0b94e
      if (!predicate(record)) {
Packit f0b94e
        return false;
Packit f0b94e
      }
Packit f0b94e
      if (record.hasPermission()) {
Packit f0b94e
        // "Clear Recent History" and the Forget button remove permissions
Packit f0b94e
        // before clearing registrations, but it's possible for the worker to
Packit f0b94e
        // resubscribe if the "dom.push.testing.ignorePermission" pref is set.
Packit f0b94e
        this._notifySubscriptionChangeObservers(record);
Packit f0b94e
      }
Packit f0b94e
      if (!record.isExpired()) {
Packit f0b94e
        // Only unregister active registrations, since we already told the
Packit f0b94e
        // server about expired ones.
Packit f0b94e
        this._backgroundUnregister(record,
Packit f0b94e
                                   Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL);
Packit f0b94e
      }
Packit f0b94e
      return true;
Packit f0b94e
    });
Packit f0b94e
  },
Packit f0b94e
};