Bug 1449132 [wpt PR 10194] - [css-grid] Fix resolution of percentage paddings and...
[gecko.git] / dom / push / PushRecord.jsm
blob99cdc36de9dc5b225383bd77d6fb9e57e4dee253
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/. */
5 "use strict";
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.");
24 /**
25  * The push subscription record, stored in IndexedDB.
26  */
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");
48     } else {
49       this.quota = Infinity;
50     }
51   },
53   resetQuota() {
54     this.quota = this.quotaApplies() ?
55                  prefs.getIntPref("maxQuotaPerSubscription") : Infinity;
56   },
58   updateQuota(lastVisit) {
59     if (this.isExpired() || !this.quotaApplies()) {
60       // Ignore updates if the registration is already expired, or isn't
61       // subject to quota.
62       return;
63     }
64     if (lastVisit < 0) {
65       // If the user cleared their history, but retained the push permission,
66       // mark the registration as expired.
67       this.quota = 0;
68       return;
69     }
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.
74       let daysElapsed =
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")
79       );
80     }
81   },
83   receivedPush(lastVisit) {
84     this.updateQuota(lastVisit);
85     this.pushCount++;
86     this.lastPush = Date.now();
87   },
89   /**
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.
93    */
94   noteRecentMessageID(id) {
95     if (this.recentMessageIDs) {
96       this.recentMessageIDs.unshift(id);
97     } else {
98       this.recentMessageIDs = [id];
99     }
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)
104     );
105     this.recentMessageIDs.length = maxRecentMessageIDs || 0;
106   },
108   hasRecentMessageID(id) {
109     return this.recentMessageIDs && this.recentMessageIDs.includes(id);
110   },
112   reduceQuota() {
113     if (!this.quotaApplies()) {
114       return;
115     }
116     this.quota = Math.max(this.quota - 1, 0);
117   },
119   /**
120    * Queries the Places database for the last time a user visited the site
121    * associated with a push registration.
122    *
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.
126    */
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.
131       return Date.now();
132     }
134     if (AppConstants.MOZ_ANDROID_HISTORY) {
135       let result = await EventDispatcher.instance.sendRequestForResult({
136         type: "History:GetPrePathLastVisitedTimeMilliseconds",
137         prePath: this.uri.prePath,
138       });
139       return result == 0 ? -Infinity : result;
140     }
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
152     ].join(",");
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
162        FROM moz_places p
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})
167       `,
168       {
169         // Restrict the query to all pages for this origin.
170         host: this.uri.host,
171         prePath: this.uri.prePath,
172       }
173     );
175     if (!rows.length) {
176       return -Infinity;
177     }
178     // Places records times in microseconds.
179     let lastVisit = rows[0].getResultByName("lastVisit");
181     return lastVisit / 1000;
182   },
184   isTabOpen() {
185     let windows = Services.wm.getEnumerator("navigator:browser");
186     while (windows.hasMoreElements()) {
187       let window = windows.getNext();
188       if (window.closed || PrivateBrowsingUtils.isWindowPrivate(window)) {
189         continue;
190       }
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) {
198           return true;
199         }
200       }
201     }
202     return false;
203   },
205   /**
206    * Indicates whether the registration can deliver push messages to its
207    * associated service worker. System subscriptions are exempt from the
208    * permission check.
209    */
210   hasPermission() {
211     if (this.systemRecord || prefs.getBoolPref("testing.ignorePermission", false)) {
212       return true;
213     }
214     let permission = Services.perms.testExactPermissionFromPrincipal(
215       this.principal, "desktop-notification");
216     return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
217   },
219   quotaChanged() {
220     if (!this.hasPermission()) {
221       return Promise.resolve(false);
222     }
223     return this.getLastVisit()
224       .then(lastVisit => lastVisit > this.lastPush);
225   },
227   quotaApplies() {
228     return !this.systemRecord;
229   },
231   isExpired() {
232     return this.quota === 0;
233   },
235   matchesOriginAttributes(pattern) {
236     if (this.systemRecord) {
237       return false;
238     }
239     return ChromeUtils.originAttributesMatchPattern(
240       this.principal.originAttributes, pattern);
241   },
243   hasAuthenticationSecret() {
244     return !!this.authenticationSecret &&
245            this.authenticationSecret.byteLength == 16;
246   },
248   matchesAppServerKey(key) {
249     if (!this.appServerKey) {
250       return !key;
251     }
252     if (!key) {
253       return false;
254     }
255     return this.appServerKey.length === key.length &&
256            this.appServerKey.every((value, index) => value === key[index]);
257   },
259   toSubscription() {
260     return {
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,
270     };
271   },
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, {
278   principal: {
279     get() {
280       if (this.systemRecord) {
281         return Services.scriptSecurityManager.getSystemPrincipal();
282       }
283       let principal = principals.get(this);
284       if (!principal) {
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);
292       }
293       return principal;
294     },
295     configurable: true,
296   },
298   uri: {
299     get() {
300       return this.principal.URI;
301     },
302     configurable: true,
303   },