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