зеркало из https://github.com/mozilla/gecko-dev.git
225 строки
7.0 KiB
JavaScript
225 строки
7.0 KiB
JavaScript
/* 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/Preferences.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
|
"resource://gre/modules/PlacesUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
|
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
|
|
"resource://gre/modules/BrowserUtils.jsm");
|
|
|
|
this.EXPORTED_SYMBOLS = ["PushRecord"];
|
|
|
|
const prefs = new Preferences("dom.push.");
|
|
|
|
// 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(",");
|
|
|
|
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.setQuota(props.quota);
|
|
}
|
|
|
|
PushRecord.prototype = {
|
|
setQuota(suggestedQuota) {
|
|
this.quota = (!isNaN(suggestedQuota) && suggestedQuota >= 0) ?
|
|
suggestedQuota : prefs.get("maxQuotaPerSubscription");
|
|
},
|
|
|
|
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;
|
|
}
|
|
let currentQuota;
|
|
if (lastVisit > this.lastPush) {
|
|
// If the user visited the site since the last time we received a
|
|
// notification, reset the quota.
|
|
let daysElapsed = (Date.now() - lastVisit) / 24 / 60 / 60 / 1000;
|
|
currentQuota = Math.min(
|
|
Math.round(8 * Math.pow(daysElapsed, -0.8)),
|
|
prefs.get("maxQuotaPerSubscription")
|
|
);
|
|
} else {
|
|
// The user hasn't visited the site since the last notification.
|
|
currentQuota = this.quota;
|
|
}
|
|
this.quota = Math.max(currentQuota - 1, 0);
|
|
},
|
|
|
|
receivedPush(lastVisit) {
|
|
this.updateQuota(lastVisit);
|
|
this.pushCount++;
|
|
this.lastPush = Date.now();
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
if (!this.quotaApplies() || this.isTabOpen()) {
|
|
// If the registration isn't subject to quota, or the user already
|
|
// has the site open, skip the Places query.
|
|
return Promise.resolve(Date.now());
|
|
}
|
|
return PlacesUtils.withConnectionWrapper("PushRecord.getLastVisit", db => {
|
|
// 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.
|
|
return db.executeCached(
|
|
`SELECT MAX(p.last_visit_date)
|
|
FROM moz_places p
|
|
INNER JOIN moz_historyvisits h ON p.id = h.place_id
|
|
WHERE (
|
|
p.url >= :urlLowerBound AND p.url <= :urlUpperBound AND
|
|
h.visit_type IN (${QUOTA_REFRESH_TRANSITIONS_SQL})
|
|
)
|
|
`,
|
|
{
|
|
// Restrict the query to all pages for this origin.
|
|
urlLowerBound: this.uri.prePath,
|
|
urlUpperBound: this.uri.prePath + "\x7f",
|
|
}
|
|
);
|
|
}).then(rows => {
|
|
if (!rows.length) {
|
|
return -Infinity;
|
|
}
|
|
// Places records times in microseconds.
|
|
let lastVisit = rows[0].getResultByIndex(0);
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Returns the push permission state for the principal associated with
|
|
* this registration.
|
|
*/
|
|
pushPermission() {
|
|
return Services.perms.testExactPermissionFromPrincipal(
|
|
this.principal, "push");
|
|
},
|
|
|
|
/**
|
|
* Indicates whether the registration can deliver push messages to its
|
|
* associated service worker.
|
|
*/
|
|
hasPermission() {
|
|
let permission = this.pushPermission();
|
|
return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
|
|
},
|
|
|
|
quotaApplies() {
|
|
return Number.isFinite(this.quota);
|
|
},
|
|
|
|
isExpired() {
|
|
return this.quota === 0;
|
|
},
|
|
|
|
toRegistration() {
|
|
return {
|
|
pushEndpoint: this.pushEndpoint,
|
|
lastPush: this.lastPush,
|
|
pushCount: this.pushCount,
|
|
};
|
|
},
|
|
|
|
toRegister() {
|
|
return {
|
|
pushEndpoint: this.pushEndpoint,
|
|
};
|
|
},
|
|
};
|
|
|
|
// Define lazy getters for the principal and scope URI. IndexedDB can't store
|
|
// `nsIPrincipal` objects, so we keep them in a private weak map.
|
|
let principals = new WeakMap();
|
|
Object.defineProperties(PushRecord.prototype, {
|
|
principal: {
|
|
get() {
|
|
let principal = principals.get(this);
|
|
if (!principal) {
|
|
let url = this.scope;
|
|
if (this.originAttributes) {
|
|
// Allow tests to omit origin attributes.
|
|
url += this.originAttributes;
|
|
}
|
|
principal = BrowserUtils.principalFromOrigin(url);
|
|
principals.set(this, principal);
|
|
}
|
|
return principal;
|
|
},
|
|
configurable: true,
|
|
},
|
|
|
|
uri: {
|
|
get() {
|
|
return this.principal.URI;
|
|
},
|
|
configurable: true,
|
|
},
|
|
});
|