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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
8 ChromeUtils.import("resource://gre/modules/Services.jsm");
9 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
11 ChromeUtils.defineModuleGetter(this, "EventDispatcher",
12 "resource://gre/modules/Messaging.jsm");
14 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
15 "resource://gre/modules/PlacesUtils.jsm");
16 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
17 "resource://gre/modules/PrivateBrowsingUtils.jsm");
20 var EXPORTED_SYMBOLS = ["PushRecord"];
22 const prefs = Services.prefs.getBranch("dom.push.");
25 * The push subscription record, stored in IndexedDB.
27 function PushRecord(props) {
28 this.pushEndpoint = props.pushEndpoint;
29 this.scope = props.scope;
30 this.originAttributes = props.originAttributes;
31 this.pushCount = props.pushCount || 0;
32 this.lastPush = props.lastPush || 0;
33 this.p256dhPublicKey = props.p256dhPublicKey;
34 this.p256dhPrivateKey = props.p256dhPrivateKey;
35 this.authenticationSecret = props.authenticationSecret;
36 this.systemRecord = !!props.systemRecord;
37 this.appServerKey = props.appServerKey;
38 this.recentMessageIDs = props.recentMessageIDs;
39 this.setQuota(props.quota);
40 this.ctime = (typeof props.ctime === "number") ? props.ctime : 0;
43 PushRecord.prototype = {
44 setQuota(suggestedQuota) {
45 if (this.quotaApplies()) {
46 let quota = +suggestedQuota;
47 this.quota = quota >= 0 ? quota : prefs.getIntPref("maxQuotaPerSubscription");
49 this.quota = Infinity;
54 this.quota = this.quotaApplies() ?
55 prefs.getIntPref("maxQuotaPerSubscription") : Infinity;
58 updateQuota(lastVisit) {
59 if (this.isExpired() || !this.quotaApplies()) {
60 // Ignore updates if the registration is already expired, or isn't
65 // If the user cleared their history, but retained the push permission,
66 // mark the registration as expired.
70 if (lastVisit > this.lastPush) {
71 // If the user visited the site since the last time we received a
72 // notification, reset the quota. `Math.max(0, ...)` ensures the
73 // last visit date isn't in the future.
75 Math.max(0, (Date.now() - lastVisit) / 24 / 60 / 60 / 1000);
76 this.quota = Math.min(
77 Math.round(8 * Math.pow(daysElapsed, -0.8)),
78 prefs.getIntPref("maxQuotaPerSubscription")
83 receivedPush(lastVisit) {
84 this.updateQuota(lastVisit);
86 this.lastPush = Date.now();
90 * Records a message ID sent to this push registration. We track the last few
91 * messages sent to each registration to avoid firing duplicate events for
92 * unacknowledged messages.
94 noteRecentMessageID(id) {
95 if (this.recentMessageIDs) {
96 this.recentMessageIDs.unshift(id);
98 this.recentMessageIDs = [id];
100 // Drop older message IDs from the end of the list.
101 let maxRecentMessageIDs = Math.min(
102 this.recentMessageIDs.length,
103 Math.max(prefs.getIntPref("maxRecentMessageIDsPerSubscription"), 0)
105 this.recentMessageIDs.length = maxRecentMessageIDs || 0;
108 hasRecentMessageID(id) {
109 return this.recentMessageIDs && this.recentMessageIDs.includes(id);
113 if (!this.quotaApplies()) {
116 this.quota = Math.max(this.quota - 1, 0);
120 * Queries the Places database for the last time a user visited the site
121 * associated with a push registration.
123 * @returns {Promise} A promise resolved with either the last time the user
124 * visited the site, or `-Infinity` if the site is not in the user's history.
125 * The time is expressed in milliseconds since Epoch.
127 async getLastVisit() {
128 if (!this.quotaApplies() || this.isTabOpen()) {
129 // If the registration isn't subject to quota, or the user already
130 // has the site open, skip expensive database queries.
134 if (AppConstants.MOZ_ANDROID_HISTORY) {
135 let result = await EventDispatcher.instance.sendRequestForResult({
136 type: "History:GetPrePathLastVisitedTimeMilliseconds",
137 prePath: this.uri.prePath,
139 return result == 0 ? -Infinity : result;
142 // Places History transition types that can fire a
143 // `pushsubscriptionchange` event when the user visits a site with expired push
144 // registrations. Visits only count if the user sees the origin in the address
145 // bar. This excludes embedded resources, downloads, and framed links.
146 const QUOTA_REFRESH_TRANSITIONS_SQL = [
147 Ci.nsINavHistoryService.TRANSITION_LINK,
148 Ci.nsINavHistoryService.TRANSITION_TYPED,
149 Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
150 Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
151 Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY
154 let db = await PlacesUtils.promiseDBConnection();
155 // We're using a custom query instead of `nsINavHistoryQueryOptions`
156 // because the latter doesn't expose a way to filter by transition type:
157 // `setTransitions` performs a logical "and," but we want an "or." We
158 // also avoid an unneeded left join with favicons, and an `ORDER BY`
159 // clause that emits a suboptimal index warning.
160 let rows = await db.executeCached(
161 `SELECT MAX(visit_date) AS lastVisit
163 JOIN moz_historyvisits ON p.id = place_id
164 WHERE rev_host = get_unreversed_host(:host || '.') || '.'
165 AND url BETWEEN :prePath AND :prePath || X'FFFF'
166 AND visit_type IN (${QUOTA_REFRESH_TRANSITIONS_SQL})
169 // Restrict the query to all pages for this origin.
171 prePath: this.uri.prePath,
178 // Places records times in microseconds.
179 let lastVisit = rows[0].getResultByName("lastVisit");
181 return lastVisit / 1000;
185 let windows = Services.wm.getEnumerator("navigator:browser");
186 while (windows.hasMoreElements()) {
187 let window = windows.getNext();
188 if (window.closed || PrivateBrowsingUtils.isWindowPrivate(window)) {
191 // `gBrowser` on Desktop; `BrowserApp` on Fennec.
192 let tabs = window.gBrowser ? window.gBrowser.tabContainer.children :
193 window.BrowserApp.tabs;
194 for (let tab of tabs) {
195 // `linkedBrowser` on Desktop; `browser` on Fennec.
196 let tabURI = (tab.linkedBrowser || tab.browser).currentURI;
197 if (tabURI.prePath == this.uri.prePath) {
206 * Indicates whether the registration can deliver push messages to its
207 * associated service worker. System subscriptions are exempt from the
211 if (this.systemRecord || prefs.getBoolPref("testing.ignorePermission", false)) {
214 let permission = Services.perms.testExactPermissionFromPrincipal(
215 this.principal, "desktop-notification");
216 return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
220 if (!this.hasPermission()) {
221 return Promise.resolve(false);
223 return this.getLastVisit()
224 .then(lastVisit => lastVisit > this.lastPush);
228 return !this.systemRecord;
232 return this.quota === 0;
235 matchesOriginAttributes(pattern) {
236 if (this.systemRecord) {
239 return ChromeUtils.originAttributesMatchPattern(
240 this.principal.originAttributes, pattern);
243 hasAuthenticationSecret() {
244 return !!this.authenticationSecret &&
245 this.authenticationSecret.byteLength == 16;
248 matchesAppServerKey(key) {
249 if (!this.appServerKey) {
255 return this.appServerKey.length === key.length &&
256 this.appServerKey.every((value, index) => value === key[index]);
261 endpoint: this.pushEndpoint,
262 lastPush: this.lastPush,
263 pushCount: this.pushCount,
264 p256dhKey: this.p256dhPublicKey,
265 p256dhPrivateKey: this.p256dhPrivateKey,
266 authenticationSecret: this.authenticationSecret,
267 appServerKey: this.appServerKey,
268 quota: this.quotaApplies() ? this.quota : -1,
269 systemRecord: this.systemRecord,
274 // Define lazy getters for the principal and scope URI. IndexedDB can't store
275 // `nsIPrincipal` objects, so we keep them in a private weak map.
276 var principals = new WeakMap();
277 Object.defineProperties(PushRecord.prototype, {
280 if (this.systemRecord) {
281 return Services.scriptSecurityManager.getSystemPrincipal();
283 let principal = principals.get(this);
285 let uri = Services.io.newURI(this.scope);
286 // Allow tests to omit origin attributes.
287 let originSuffix = this.originAttributes || "";
288 let originAttributes =
289 principal = Services.scriptSecurityManager.createCodebasePrincipal(uri,
290 ChromeUtils.createOriginAttributesFromOrigin(originSuffix));
291 principals.set(this, principal);
300 return this.principal.URI;