Blame dom/push/PushRecord.jsm

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/Services.jsm");
Packit f0b94e
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
Packit f0b94e
Packit f0b94e
ChromeUtils.defineModuleGetter(this, "EventDispatcher",
Packit f0b94e
                               "resource://gre/modules/Messaging.jsm");
Packit f0b94e
Packit f0b94e
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
Packit f0b94e
                               "resource://gre/modules/PlacesUtils.jsm");
Packit f0b94e
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
Packit f0b94e
                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
Packit f0b94e
Packit f0b94e
Packit f0b94e
var EXPORTED_SYMBOLS = ["PushRecord"];
Packit f0b94e
Packit f0b94e
const prefs = Services.prefs.getBranch("dom.push.");
Packit f0b94e
Packit f0b94e
/**
Packit f0b94e
 * The push subscription record, stored in IndexedDB.
Packit f0b94e
 */
Packit f0b94e
function PushRecord(props) {
Packit f0b94e
  this.pushEndpoint = props.pushEndpoint;
Packit f0b94e
  this.scope = props.scope;
Packit f0b94e
  this.originAttributes = props.originAttributes;
Packit f0b94e
  this.pushCount = props.pushCount || 0;
Packit f0b94e
  this.lastPush = props.lastPush || 0;
Packit f0b94e
  this.p256dhPublicKey = props.p256dhPublicKey;
Packit f0b94e
  this.p256dhPrivateKey = props.p256dhPrivateKey;
Packit f0b94e
  this.authenticationSecret = props.authenticationSecret;
Packit f0b94e
  this.systemRecord = !!props.systemRecord;
Packit f0b94e
  this.appServerKey = props.appServerKey;
Packit f0b94e
  this.recentMessageIDs = props.recentMessageIDs;
Packit f0b94e
  this.setQuota(props.quota);
Packit f0b94e
  this.ctime = (typeof props.ctime === "number") ? props.ctime : 0;
Packit f0b94e
}
Packit f0b94e
Packit f0b94e
PushRecord.prototype = {
Packit f0b94e
  setQuota(suggestedQuota) {
Packit f0b94e
    if (this.quotaApplies()) {
Packit f0b94e
      let quota = +suggestedQuota;
Packit f0b94e
      this.quota = quota >= 0 ? quota : prefs.getIntPref("maxQuotaPerSubscription");
Packit f0b94e
    } else {
Packit f0b94e
      this.quota = Infinity;
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  resetQuota() {
Packit f0b94e
    this.quota = this.quotaApplies() ?
Packit f0b94e
                 prefs.getIntPref("maxQuotaPerSubscription") : Infinity;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  updateQuota(lastVisit) {
Packit f0b94e
    if (this.isExpired() || !this.quotaApplies()) {
Packit f0b94e
      // Ignore updates if the registration is already expired, or isn't
Packit f0b94e
      // subject to quota.
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    if (lastVisit < 0) {
Packit f0b94e
      // If the user cleared their history, but retained the push permission,
Packit f0b94e
      // mark the registration as expired.
Packit f0b94e
      this.quota = 0;
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    if (lastVisit > this.lastPush) {
Packit f0b94e
      // If the user visited the site since the last time we received a
Packit f0b94e
      // notification, reset the quota. `Math.max(0, ...)` ensures the
Packit f0b94e
      // last visit date isn't in the future.
Packit f0b94e
      let daysElapsed =
Packit f0b94e
        Math.max(0, (Date.now() - lastVisit) / 24 / 60 / 60 / 1000);
Packit f0b94e
      this.quota = Math.min(
Packit f0b94e
        Math.round(8 * Math.pow(daysElapsed, -0.8)),
Packit f0b94e
        prefs.getIntPref("maxQuotaPerSubscription")
Packit f0b94e
      );
Packit f0b94e
    }
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  receivedPush(lastVisit) {
Packit f0b94e
    this.updateQuota(lastVisit);
Packit f0b94e
    this.pushCount++;
Packit f0b94e
    this.lastPush = Date.now();
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Records a message ID sent to this push registration. We track the last few
Packit f0b94e
   * messages sent to each registration to avoid firing duplicate events for
Packit f0b94e
   * unacknowledged messages.
Packit f0b94e
   */
Packit f0b94e
  noteRecentMessageID(id) {
Packit f0b94e
    if (this.recentMessageIDs) {
Packit f0b94e
      this.recentMessageIDs.unshift(id);
Packit f0b94e
    } else {
Packit f0b94e
      this.recentMessageIDs = [id];
Packit f0b94e
    }
Packit f0b94e
    // Drop older message IDs from the end of the list.
Packit f0b94e
    let maxRecentMessageIDs = Math.min(
Packit f0b94e
      this.recentMessageIDs.length,
Packit f0b94e
      Math.max(prefs.getIntPref("maxRecentMessageIDsPerSubscription"), 0)
Packit f0b94e
    );
Packit f0b94e
    this.recentMessageIDs.length = maxRecentMessageIDs || 0;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  hasRecentMessageID(id) {
Packit f0b94e
    return this.recentMessageIDs && this.recentMessageIDs.includes(id);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  reduceQuota() {
Packit f0b94e
    if (!this.quotaApplies()) {
Packit f0b94e
      return;
Packit f0b94e
    }
Packit f0b94e
    this.quota = Math.max(this.quota - 1, 0);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Queries the Places database for the last time a user visited the site
Packit f0b94e
   * associated with a push registration.
Packit f0b94e
   *
Packit f0b94e
   * @returns {Promise} A promise resolved with either the last time the user
Packit f0b94e
   *  visited the site, or `-Infinity` if the site is not in the user's history.
Packit f0b94e
   *  The time is expressed in milliseconds since Epoch.
Packit f0b94e
   */
Packit f0b94e
  async getLastVisit() {
Packit f0b94e
    if (!this.quotaApplies() || this.isTabOpen()) {
Packit f0b94e
      // If the registration isn't subject to quota, or the user already
Packit f0b94e
      // has the site open, skip expensive database queries.
Packit f0b94e
      return Date.now();
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    if (AppConstants.MOZ_ANDROID_HISTORY) {
Packit f0b94e
      let result = await EventDispatcher.instance.sendRequestForResult({
Packit f0b94e
        type: "History:GetPrePathLastVisitedTimeMilliseconds",
Packit f0b94e
        prePath: this.uri.prePath,
Packit f0b94e
      });
Packit f0b94e
      return result == 0 ? -Infinity : result;
Packit f0b94e
    }
Packit f0b94e
Packit f0b94e
    // Places History transition types that can fire a
Packit f0b94e
    // `pushsubscriptionchange` event when the user visits a site with expired push
Packit f0b94e
    // registrations. Visits only count if the user sees the origin in the address
Packit f0b94e
    // bar. This excludes embedded resources, downloads, and framed links.
Packit f0b94e
    const QUOTA_REFRESH_TRANSITIONS_SQL = [
Packit f0b94e
      Ci.nsINavHistoryService.TRANSITION_LINK,
Packit f0b94e
      Ci.nsINavHistoryService.TRANSITION_TYPED,
Packit f0b94e
      Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
Packit f0b94e
      Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
Packit f0b94e
      Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY
Packit f0b94e
    ].join(",");
Packit f0b94e
Packit f0b94e
    let db =  await PlacesUtils.promiseDBConnection();
Packit f0b94e
    // We're using a custom query instead of `nsINavHistoryQueryOptions`
Packit f0b94e
    // because the latter doesn't expose a way to filter by transition type:
Packit f0b94e
    // `setTransitions` performs a logical "and," but we want an "or." We
Packit f0b94e
    // also avoid an unneeded left join with favicons, and an `ORDER BY`
Packit f0b94e
    // clause that emits a suboptimal index warning.
Packit f0b94e
    let rows = await db.executeCached(
Packit f0b94e
      `SELECT MAX(visit_date) AS lastVisit
Packit f0b94e
       FROM moz_places p
Packit f0b94e
       JOIN moz_historyvisits ON p.id = place_id
Packit f0b94e
       WHERE rev_host = get_unreversed_host(:host || '.') || '.'
Packit f0b94e
         AND url BETWEEN :prePath AND :prePath || X'FFFF'
Packit f0b94e
         AND visit_type IN (${QUOTA_REFRESH_TRANSITIONS_SQL})
Packit f0b94e
      `,
Packit f0b94e
      {
Packit f0b94e
        // Restrict the query to all pages for this origin.
Packit f0b94e
        host: this.uri.host,
Packit f0b94e
        prePath: this.uri.prePath,
Packit f0b94e
      }
Packit f0b94e
    );
Packit f0b94e
Packit f0b94e
    if (!rows.length) {
Packit f0b94e
      return -Infinity;
Packit f0b94e
    }
Packit f0b94e
    // Places records times in microseconds.
Packit f0b94e
    let lastVisit = rows[0].getResultByName("lastVisit");
Packit f0b94e
Packit f0b94e
    return lastVisit / 1000;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  isTabOpen() {
Packit f0b94e
    let windows = Services.wm.getEnumerator("navigator:browser");
Packit f0b94e
    while (windows.hasMoreElements()) {
Packit f0b94e
      let window = windows.getNext();
Packit f0b94e
      if (window.closed || PrivateBrowsingUtils.isWindowPrivate(window)) {
Packit f0b94e
        continue;
Packit f0b94e
      }
Packit f0b94e
      // `gBrowser` on Desktop; `BrowserApp` on Fennec.
Packit f0b94e
      let tabs = window.gBrowser ? window.gBrowser.tabContainer.children :
Packit f0b94e
                 window.BrowserApp.tabs;
Packit f0b94e
      for (let tab of tabs) {
Packit f0b94e
        // `linkedBrowser` on Desktop; `browser` on Fennec.
Packit f0b94e
        let tabURI = (tab.linkedBrowser || tab.browser).currentURI;
Packit f0b94e
        if (tabURI.prePath == this.uri.prePath) {
Packit f0b94e
          return true;
Packit f0b94e
        }
Packit f0b94e
      }
Packit f0b94e
    }
Packit f0b94e
    return false;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  /**
Packit f0b94e
   * Indicates whether the registration can deliver push messages to its
Packit f0b94e
   * associated service worker. System subscriptions are exempt from the
Packit f0b94e
   * permission check.
Packit f0b94e
   */
Packit f0b94e
  hasPermission() {
Packit f0b94e
    if (this.systemRecord || prefs.getBoolPref("testing.ignorePermission", false)) {
Packit f0b94e
      return true;
Packit f0b94e
    }
Packit f0b94e
    let permission = Services.perms.testExactPermissionFromPrincipal(
Packit f0b94e
      this.principal, "desktop-notification");
Packit f0b94e
    return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  quotaChanged() {
Packit f0b94e
    if (!this.hasPermission()) {
Packit f0b94e
      return Promise.resolve(false);
Packit f0b94e
    }
Packit f0b94e
    return this.getLastVisit()
Packit f0b94e
      .then(lastVisit => lastVisit > this.lastPush);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  quotaApplies() {
Packit f0b94e
    return !this.systemRecord;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  isExpired() {
Packit f0b94e
    return this.quota === 0;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  matchesOriginAttributes(pattern) {
Packit f0b94e
    if (this.systemRecord) {
Packit f0b94e
      return false;
Packit f0b94e
    }
Packit f0b94e
    return ChromeUtils.originAttributesMatchPattern(
Packit f0b94e
      this.principal.originAttributes, pattern);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  hasAuthenticationSecret() {
Packit f0b94e
    return !!this.authenticationSecret &&
Packit f0b94e
           this.authenticationSecret.byteLength == 16;
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  matchesAppServerKey(key) {
Packit f0b94e
    if (!this.appServerKey) {
Packit f0b94e
      return !key;
Packit f0b94e
    }
Packit f0b94e
    if (!key) {
Packit f0b94e
      return false;
Packit f0b94e
    }
Packit f0b94e
    return this.appServerKey.length === key.length &&
Packit f0b94e
           this.appServerKey.every((value, index) => value === key[index]);
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  toSubscription() {
Packit f0b94e
    return {
Packit f0b94e
      endpoint: this.pushEndpoint,
Packit f0b94e
      lastPush: this.lastPush,
Packit f0b94e
      pushCount: this.pushCount,
Packit f0b94e
      p256dhKey: this.p256dhPublicKey,
Packit f0b94e
      p256dhPrivateKey: this.p256dhPrivateKey,
Packit f0b94e
      authenticationSecret: this.authenticationSecret,
Packit f0b94e
      appServerKey: this.appServerKey,
Packit f0b94e
      quota: this.quotaApplies() ? this.quota : -1,
Packit f0b94e
      systemRecord: this.systemRecord,
Packit f0b94e
    };
Packit f0b94e
  },
Packit f0b94e
};
Packit f0b94e
Packit f0b94e
// Define lazy getters for the principal and scope URI. IndexedDB can't store
Packit f0b94e
// `nsIPrincipal` objects, so we keep them in a private weak map.
Packit f0b94e
var principals = new WeakMap();
Packit f0b94e
Object.defineProperties(PushRecord.prototype, {
Packit f0b94e
  principal: {
Packit f0b94e
    get() {
Packit f0b94e
      if (this.systemRecord) {
Packit f0b94e
        return Services.scriptSecurityManager.getSystemPrincipal();
Packit f0b94e
      }
Packit f0b94e
      let principal = principals.get(this);
Packit f0b94e
      if (!principal) {
Packit f0b94e
        let uri = Services.io.newURI(this.scope);
Packit f0b94e
        // Allow tests to omit origin attributes.
Packit f0b94e
        let originSuffix = this.originAttributes || "";
Packit f0b94e
        let originAttributes =
Packit f0b94e
        principal = Services.scriptSecurityManager.createCodebasePrincipal(uri,
Packit f0b94e
          ChromeUtils.createOriginAttributesFromOrigin(originSuffix));
Packit f0b94e
        principals.set(this, principal);
Packit f0b94e
      }
Packit f0b94e
      return principal;
Packit f0b94e
    },
Packit f0b94e
    configurable: true,
Packit f0b94e
  },
Packit f0b94e
Packit f0b94e
  uri: {
Packit f0b94e
    get() {
Packit f0b94e
      return this.principal.URI;
Packit f0b94e
    },
Packit f0b94e
    configurable: true,
Packit f0b94e
  },
Packit f0b94e
});