|
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 |
});
|