зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1153504
- Add per-origin push quotas. r=nsm,mt,markh
--HG-- extra : commitid : 8CPpVl9sAYo extra : rebase_source : d4e6330e3717c89f39686c620ae0aeb90e2156c8
This commit is contained in:
Родитель
74e29ff0f6
Коммит
97018538cd
|
@ -23,10 +23,11 @@ const prefs = new Preferences("dom.push.");
|
|||
|
||||
this.EXPORTED_SYMBOLS = ["PushDB"];
|
||||
|
||||
this.PushDB = function PushDB(dbName, dbVersion, dbStoreName, schemaFunction) {
|
||||
this.PushDB = function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
|
||||
debug("PushDB()");
|
||||
this._dbStoreName = dbStoreName;
|
||||
this._schemaFunction = schemaFunction;
|
||||
this._keyPath = keyPath;
|
||||
this._model = model;
|
||||
|
||||
// set the indexeddb database
|
||||
this.initDBHelper(dbName, dbVersion,
|
||||
|
@ -38,9 +39,48 @@ this.PushDB = function PushDB(dbName, dbVersion, dbStoreName, schemaFunction) {
|
|||
this.PushDB.prototype = {
|
||||
__proto__: IndexedDBHelper.prototype,
|
||||
|
||||
toPushRecord: function(record) {
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
return new this._model(record);
|
||||
},
|
||||
|
||||
isValidRecord: function(record) {
|
||||
return record && typeof record.scope == "string" &&
|
||||
typeof record.originAttributes == "string" &&
|
||||
record.quota >= 0 &&
|
||||
typeof record[this._keyPath] == "string";
|
||||
},
|
||||
|
||||
upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
|
||||
if (this._schemaFunction) {
|
||||
this._schemaFunction(aTransaction, aDb, aOldVersion, aNewVersion, this);
|
||||
if (aOldVersion <= 3) {
|
||||
//XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
|
||||
//registrations away without even informing the app.
|
||||
if (aDb.objectStoreNames.contains(this._dbStoreName)) {
|
||||
aDb.deleteObjectStore(this._dbStoreName);
|
||||
}
|
||||
|
||||
let objectStore = aDb.createObjectStore(this._dbStoreName,
|
||||
{ keyPath: this._keyPath });
|
||||
|
||||
// index to fetch records based on endpoints. used by unregister
|
||||
objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
|
||||
|
||||
// index to fetch records by identifiers.
|
||||
// In the current security model, the originAttributes distinguish between
|
||||
// different 'apps' on the same origin. Since ServiceWorkers are
|
||||
// same-origin to the scope they are registered for, the attributes and
|
||||
// scope are enough to reconstruct a valid principal.
|
||||
objectStore.createIndex("identifiers", ["scope", "originAttributes"], { unique: true });
|
||||
objectStore.createIndex("originAttributes", "originAttributes", { unique: false });
|
||||
}
|
||||
|
||||
if (aOldVersion < 4) {
|
||||
let objectStore = aTransaction.objectStore(this._dbStoreName);
|
||||
|
||||
// index to fetch active and expired registrations.
|
||||
objectStore.createIndex("quota", "quota", { unique: false });
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -51,15 +91,25 @@ this.PushDB.prototype = {
|
|||
|
||||
put: function(aRecord) {
|
||||
debug("put()" + JSON.stringify(aRecord));
|
||||
if (!this.isValidRecord(aRecord)) {
|
||||
return Promise.reject(new TypeError(
|
||||
"Scope, originAttributes, and quota are required! " +
|
||||
JSON.stringify(aRecord)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
function txnCb(aTxn, aStore) {
|
||||
aStore.put(aRecord).onsuccess = function setTxnResult(aEvent) {
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
aStore.put(aRecord).onsuccess = aEvent => {
|
||||
debug("Request successful. Updated record ID: " +
|
||||
aEvent.target.result);
|
||||
aTxn.result = this.toPushRecord(aRecord);
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
|
@ -111,12 +161,12 @@ this.PushDB.prototype = {
|
|||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
function txnCb(aTxn, aStore) {
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let index = aStore.index("pushEndpoint");
|
||||
index.get(aPushEndpoint).onsuccess = function setTxnResult(aEvent) {
|
||||
aTxn.result = aEvent.target.result;
|
||||
index.get(aPushEndpoint).onsuccess = aEvent => {
|
||||
aTxn.result = this.toPushRecord(aEvent.target.result);
|
||||
debug("Fetch successful " + aEvent.target.result);
|
||||
};
|
||||
},
|
||||
|
@ -133,11 +183,11 @@ this.PushDB.prototype = {
|
|||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
function txnCb(aTxn, aStore) {
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
aStore.get(aKeyID).onsuccess = function setTxnResult(aEvent) {
|
||||
aTxn.result = aEvent.target.result;
|
||||
aStore.get(aKeyID).onsuccess = aEvent => {
|
||||
aTxn.result = this.toPushRecord(aEvent.target.result);
|
||||
debug("Fetch successful " + aEvent.target.result);
|
||||
};
|
||||
},
|
||||
|
@ -161,14 +211,14 @@ this.PushDB.prototype = {
|
|||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
function txnCb(aTxn, aStore) {
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let index = aStore.index("identifiers");
|
||||
let request = index.get(IDBKeyRange.only([aPageRecord.scope, aPageRecord.originAttributes]));
|
||||
request.onsuccess = function setTxnResult(aEvent) {
|
||||
aTxn.result = aEvent.target.result;
|
||||
}
|
||||
request.onsuccess = aEvent => {
|
||||
aTxn.result = this.toPushRecord(aEvent.target.result);
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
|
@ -181,7 +231,7 @@ this.PushDB.prototype = {
|
|||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
function txnCb(aTxn, aStore) {
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let index = aStore.index(aKeyName);
|
||||
|
@ -189,9 +239,10 @@ this.PushDB.prototype = {
|
|||
// high storage APIs, we don't expect more than a handful of
|
||||
// registrations per domain, and usually only one.
|
||||
let getAllReq = index.mozGetAll(aKeyValue);
|
||||
getAllReq.onsuccess = function setTxnResult(aEvent) {
|
||||
aTxn.result = aEvent.target.result;
|
||||
}
|
||||
getAllReq.onsuccess = aEvent => {
|
||||
aTxn.result = aEvent.target.result.map(
|
||||
record => this.toPushRecord(record));
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
|
@ -214,10 +265,88 @@ this.PushDB.prototype = {
|
|||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
function txnCb(aTxn, aStore) {
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
aStore.mozGetAll().onsuccess = function(event) {
|
||||
aTxn.result = event.target.result;
|
||||
aStore.mozGetAll().onsuccess = event => {
|
||||
aTxn.result = event.target.result.map(
|
||||
record => this.toPushRecord(record));
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_getAllByPushQuota: function(range) {
|
||||
debug("getAllByPushQuota()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = [];
|
||||
|
||||
let index = aStore.index("quota");
|
||||
index.openCursor(range).onsuccess = event => {
|
||||
let cursor = event.target.result;
|
||||
if (cursor) {
|
||||
aTxn.result.push(this.toPushRecord(cursor.value));
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
getAllUnexpired: function() {
|
||||
debug("getAllUnexpired()");
|
||||
return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
|
||||
},
|
||||
|
||||
getAllExpired: function() {
|
||||
debug("getAllExpired()");
|
||||
return this._getAllByPushQuota(IDBKeyRange.only(0));
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an existing push registration.
|
||||
*
|
||||
* @param {String} aKeyID The registration ID.
|
||||
* @param {Function} aUpdateFunc A function that receives the existing
|
||||
* registration record as its argument, and returns a new record. If the
|
||||
* function returns `null` or `undefined`, the record will not be updated.
|
||||
* If the record does not exist, the function will not be called.
|
||||
* @returns {Promise} A promise resolved with either the updated record, or
|
||||
* `undefined` if the record was not updated.
|
||||
*/
|
||||
update: function(aKeyID, aUpdateFunc) {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aStore.get(aKeyID).onsuccess = aEvent => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let record = aEvent.target.result;
|
||||
if (!record) {
|
||||
debug("update: Key ID " + aKeyID + " does not exist");
|
||||
return;
|
||||
}
|
||||
let newRecord = aUpdateFunc(this.toPushRecord(record));
|
||||
if (!this.isValidRecord(newRecord)) {
|
||||
debug("update: Ignoring invalid update for key ID " + aKeyID);
|
||||
return;
|
||||
}
|
||||
aStore.put(newRecord).onsuccess = aEvent => {
|
||||
debug("update: Update successful for key ID " + aKeyID);
|
||||
aTxn.result = newRecord;
|
||||
};
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
|
|
|
@ -39,7 +39,11 @@ PushNotificationService.prototype = {
|
|||
Ci.nsIPushNotificationService]),
|
||||
|
||||
register: function register(scope, originAttributes) {
|
||||
return PushService._register({scope, originAttributes});
|
||||
return PushService._register({
|
||||
scope: scope,
|
||||
originAttributes: originAttributes,
|
||||
maxQuota: Infinity,
|
||||
});
|
||||
},
|
||||
|
||||
unregister: function unregister(scope, originAttributes) {
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
/* 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");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PushRecord"];
|
||||
|
||||
const prefs = new Preferences("dom.push.");
|
||||
|
||||
// History transition types that can fire an `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.origin = Services.io.newURI(this.scope, null, null).prePath;
|
||||
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.origin,
|
||||
urlUpperBound: this.origin + "\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.origin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Mark the `origin` property as non-enumerable to avoid storing the
|
||||
// registration origin in IndexedDB.
|
||||
Object.defineProperty(PushRecord.prototype, "origin", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
});
|
|
@ -83,6 +83,7 @@ this.PushService = {
|
|||
_state: PUSH_SERVICE_UNINIT,
|
||||
_db: null,
|
||||
_options: null,
|
||||
_alarmID: null,
|
||||
|
||||
// When serverURI changes (this is used for testing), db is cleaned up and a
|
||||
// a new db is started. This events must be sequential.
|
||||
|
@ -181,11 +182,11 @@ this.PushService = {
|
|||
// Disconnect first.
|
||||
this._service.disconnect();
|
||||
}
|
||||
this._db.getAllKeyIDs()
|
||||
.then(keyIDs => {
|
||||
if (keyIDs.length > 0) {
|
||||
this._db.getAllUnexpired()
|
||||
.then(records => {
|
||||
if (records.length > 0) {
|
||||
// if there are request waiting
|
||||
this._service.connect(keyIDs);
|
||||
this._service.connect(records);
|
||||
}
|
||||
});
|
||||
this._setState(PUSH_SERVICE_RUNNING);
|
||||
|
@ -242,6 +243,10 @@ this.PushService = {
|
|||
}
|
||||
break;
|
||||
|
||||
case "idle-daily":
|
||||
this._dropExpiredRegistrations();
|
||||
break;
|
||||
|
||||
case "webapps-clear-data":
|
||||
debug("webapps-clear-data");
|
||||
|
||||
|
@ -259,26 +264,26 @@ this.PushService = {
|
|||
this._db.getAllByOriginAttributes(originAttributes)
|
||||
.then(records => {
|
||||
records.forEach(record => {
|
||||
this._db.delete(this._service.getKeyFromRecord(record))
|
||||
this._db.delete(record.keyID)
|
||||
.then(_ => {
|
||||
// courtesy, but don't establish a connection
|
||||
// just for it
|
||||
if (this._ws) {
|
||||
debug("Had a connection, so telling the server");
|
||||
this._sendRequest("unregister", {channelID: records.channelID})
|
||||
this._sendRequest("unregister", {channelID: record.channelID})
|
||||
.catch(function(e) {
|
||||
debug("Unregister errored " + e);
|
||||
});
|
||||
}
|
||||
}, err => {
|
||||
debug("webapps-clear-data: " + scope +
|
||||
" Could not delete entry " + records.channelID);
|
||||
debug("webapps-clear-data: " + record.scope +
|
||||
" Could not delete entry " + record.channelID);
|
||||
|
||||
// courtesy, but don't establish a connection
|
||||
// just for it
|
||||
if (this._ws) {
|
||||
debug("Had a connection, so telling the server");
|
||||
this._sendRequest("unregister", {channelID: records.channelID})
|
||||
this._sendRequest("unregister", {channelID: record.channelID})
|
||||
.catch(function(e) {
|
||||
debug("Unregister errored " + e);
|
||||
});
|
||||
|
@ -437,10 +442,11 @@ this.PushService = {
|
|||
}
|
||||
|
||||
// Start service.
|
||||
this._startService(service, uri, false, options);
|
||||
// Before completing the activation check prefs. This will first check
|
||||
// connection.enabled pref and then check offline state.
|
||||
this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"));
|
||||
this._startService(service, uri, false, options).then(_ => {
|
||||
// Before completing the activation check prefs. This will first check
|
||||
// connection.enabled pref and then check offline state.
|
||||
this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"));
|
||||
});
|
||||
|
||||
} else {
|
||||
// This is only used for testing. Different tests require connecting to
|
||||
|
@ -459,8 +465,6 @@ this.PushService = {
|
|||
return;
|
||||
}
|
||||
|
||||
this._alarmID = null;
|
||||
|
||||
Services.obs.addObserver(this, "webapps-clear-data", false);
|
||||
|
||||
// On B2G the NetworkManager interface fires a network-active-changed
|
||||
|
@ -486,6 +490,9 @@ this.PushService = {
|
|||
|
||||
// Used to monitor if the user wishes to disable Push.
|
||||
prefs.observe("connection.enabled", this);
|
||||
|
||||
// Used to prune expired registrations and notify dormant service workers.
|
||||
Services.obs.addObserver(this, "idle-daily", false);
|
||||
},
|
||||
|
||||
_startService: function(service, serverURI, event, options = {}) {
|
||||
|
@ -582,7 +589,8 @@ this.PushService = {
|
|||
prefs.ignore("connection.enabled", this);
|
||||
|
||||
Services.obs.removeObserver(this, this._networkStateChangeEventName);
|
||||
Services.obs.removeObserver(this, "webapps-clear-data", false);
|
||||
Services.obs.removeObserver(this, "webapps-clear-data");
|
||||
Services.obs.removeObserver(this, "idle-daily");
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
|
@ -595,7 +603,7 @@ this.PushService = {
|
|||
this._setState(PUSH_SERVICE_UNINIT);
|
||||
|
||||
prefs.ignore("serverURL", this);
|
||||
Services.obs.removeObserver(this, "xpcom-shutdown", false);
|
||||
Services.obs.removeObserver(this, "xpcom-shutdown");
|
||||
|
||||
this._serverURIProcessEnqueue(_ =>
|
||||
this._changeServerURL("", UNINIT_EVENT));
|
||||
|
@ -653,78 +661,108 @@ this.PushService = {
|
|||
.then(_ => this._db.drop());
|
||||
},
|
||||
|
||||
_notifySubscriptionChangeObservers: function(record) {
|
||||
let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
|
||||
.getService(Ci.nsIMessageListenerManager);
|
||||
// Notify XPCOM observers.
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"push-subscription-change",
|
||||
record.scope
|
||||
);
|
||||
|
||||
let data = {
|
||||
originAttributes: record.originAttributes,
|
||||
scope: record.scope
|
||||
};
|
||||
|
||||
globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
|
||||
},
|
||||
|
||||
// Fires a push-register system message to all applications that have
|
||||
// registration.
|
||||
_notifyAllAppsRegister: function() {
|
||||
debug("notifyAllAppsRegister()");
|
||||
// records are objects describing the registration as stored in IndexedDB.
|
||||
return this._db.getAllKeyIDs()
|
||||
.then(records => {
|
||||
let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
|
||||
.getService(Ci.nsIMessageListenerManager);
|
||||
for (let record of records) {
|
||||
// Notify XPCOM observers.
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"push-subscription-change",
|
||||
scope
|
||||
);
|
||||
|
||||
let data = {
|
||||
originAttributes: record.originAttributes,
|
||||
scope: record.scope
|
||||
};
|
||||
|
||||
globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
|
||||
}
|
||||
});
|
||||
return this._db.getAllUnexpired().then(records =>
|
||||
records.forEach(record =>
|
||||
this._notifySubscriptionChangeObservers(record)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
dropRegistrationAndNotifyApp: function(aKeyId) {
|
||||
return this._db.getByKeyID(aKeyId)
|
||||
.then(record => {
|
||||
let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
|
||||
.getService(Ci.nsIMessageListenerManager);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"push-subscription-change",
|
||||
record.scope
|
||||
);
|
||||
|
||||
let data = {
|
||||
originAttributes: record.originAttributes,
|
||||
scope: record.scope
|
||||
};
|
||||
|
||||
globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
|
||||
})
|
||||
.then(_ => this._db.delete(aKeyId));
|
||||
return this._db.getByKeyID(aKeyId).then(record => {
|
||||
this._notifySubscriptionChangeObservers(record);
|
||||
return this._db.delete(aKeyId);
|
||||
});
|
||||
},
|
||||
|
||||
updateRegistrationAndNotifyApp: function(aOldKey, aRecord) {
|
||||
return this._db.delete(aOldKey)
|
||||
.then(_ => this._db.put(aRecord)
|
||||
.then(record => {
|
||||
let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
|
||||
.getService(Ci.nsIMessageListenerManager);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"push-subscription-change",
|
||||
record.scope
|
||||
);
|
||||
|
||||
let data = {
|
||||
originAttributes: record.originAttributes,
|
||||
scope: record.scope
|
||||
};
|
||||
|
||||
globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
|
||||
}));
|
||||
.then(_ => this._db.put(aRecord))
|
||||
.then(record => this._notifySubscriptionChangeObservers(record));
|
||||
},
|
||||
|
||||
receivedPushMessage: function(aPushRecord, message) {
|
||||
this._db.put(aPushRecord)
|
||||
.then(_ => this._notifyApp(aPushRecord, message));
|
||||
/**
|
||||
* Dispatches an incoming message to a service worker, recalculating the
|
||||
* quota for the associated push registration. If the quota is exceeded,
|
||||
* the registration and message will be dropped, and the worker will not
|
||||
* be notified.
|
||||
*
|
||||
* @param {String} keyID The push registration ID.
|
||||
* @param {String} message The message contents.
|
||||
* @param {Function} updateFunc A function that receives the existing
|
||||
* registration record as its argument, and returns a new record. If the
|
||||
* function returns `null` or `undefined`, the record will not be updated.
|
||||
* `PushServiceWebSocket` uses this to drop incoming updates with older
|
||||
* versions.
|
||||
*/
|
||||
receivedPushMessage: function(keyID, message, updateFunc) {
|
||||
debug("receivedPushMessage()");
|
||||
|
||||
let shouldNotify = false;
|
||||
this.getByKeyID(keyID).then(record => {
|
||||
if (!record) {
|
||||
throw new Error("No record for key ID " + keyID);
|
||||
}
|
||||
return record.getLastVisit();
|
||||
}).then(lastVisit => {
|
||||
// As a special case, don't notify the service worker if the user
|
||||
// cleared their history.
|
||||
shouldNotify = isFinite(lastVisit);
|
||||
return this._db.update(keyID, record => {
|
||||
let newRecord = updateFunc(record);
|
||||
if (!newRecord) {
|
||||
return null;
|
||||
}
|
||||
if (newRecord.isExpired()) {
|
||||
// Because `unregister` is advisory only, we can still receive messages
|
||||
// for stale registrations from the server.
|
||||
debug("receivedPushMessage: Ignoring update for expired key ID " + keyID);
|
||||
return null;
|
||||
}
|
||||
newRecord.receivedPush(lastVisit);
|
||||
return newRecord;
|
||||
});
|
||||
}).then(record => {
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
if (shouldNotify) {
|
||||
this._notifyApp(record, message);
|
||||
}
|
||||
if (record.isExpired()) {
|
||||
// Drop the registration in the background. If the user returns to the
|
||||
// site, the service worker will be notified on the next `idle-daily`
|
||||
// event.
|
||||
this._sendRequest("unregister", record).catch(error => {
|
||||
debug("receivedPushMessage: Unregister error: " + error);
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
debug("receivedPushMessage: Error notifying app: " + error);
|
||||
});
|
||||
},
|
||||
|
||||
_notifyApp: function(aPushRecord, message) {
|
||||
|
@ -775,8 +813,8 @@ this.PushService = {
|
|||
return this._db.getByKeyID(aKeyID);
|
||||
},
|
||||
|
||||
getAllKeyIDs: function() {
|
||||
return this._db.getAllKeyIDs();
|
||||
getAllUnexpired: function() {
|
||||
return this._db.getAllUnexpired();
|
||||
},
|
||||
|
||||
_sendRequest: function(action, aRecord) {
|
||||
|
@ -796,11 +834,11 @@ this.PushService = {
|
|||
debug("registerWithServer()" + JSON.stringify(aPageRecord));
|
||||
|
||||
return this._sendRequest("register", aPageRecord)
|
||||
.then(pushRecord => this._onRegisterSuccess(pushRecord),
|
||||
.then(record => this._onRegisterSuccess(record),
|
||||
err => this._onRegisterError(err))
|
||||
.then(pushRecord => {
|
||||
.then(record => {
|
||||
this._deletePendingRequest(aPageRecord);
|
||||
return pushRecord;
|
||||
return record;
|
||||
}, err => {
|
||||
this._deletePendingRequest(aPageRecord);
|
||||
throw err;
|
||||
|
@ -815,11 +853,23 @@ this.PushService = {
|
|||
|
||||
return this._checkActivated()
|
||||
.then(_ => this._db.getByIdentifiers(aPageRecord))
|
||||
.then(pushRecord => {
|
||||
if (pushRecord === undefined) {
|
||||
.then(record => {
|
||||
if (!record) {
|
||||
return this._lookupOrPutPendingRequest(aPageRecord);
|
||||
}
|
||||
return pushRecord;
|
||||
if (record.isExpired()) {
|
||||
return record.getLastVisit().then(lastVisit => {
|
||||
if (lastVisit > record.lastPush) {
|
||||
// If the user revisited the site, drop the expired push
|
||||
// registration and re-register.
|
||||
return this._db.delete(record.keyID).then(_ => {
|
||||
return this._lookupOrPutPendingRequest(aPageRecord);
|
||||
});
|
||||
}
|
||||
throw {state: 0, error: "NotFoundError"};
|
||||
});
|
||||
}
|
||||
return record;
|
||||
}, error => {
|
||||
debug("getByIdentifiers failed");
|
||||
throw error;
|
||||
|
@ -834,7 +884,7 @@ this.PushService = {
|
|||
debug("_onRegisterSuccess()");
|
||||
|
||||
return this._db.put(aRecord)
|
||||
.then(_ => aRecord, error => {
|
||||
.catch(error => {
|
||||
// Unable to save. Destroy the subscription in the background.
|
||||
this._sendRequest("unregister", aRecord).catch(err => {
|
||||
debug("_onRegisterSuccess: Error unregistering stale subscription" +
|
||||
|
@ -877,7 +927,7 @@ this.PushService = {
|
|||
if (!principal) {
|
||||
debug("No principal passed!");
|
||||
let message = {
|
||||
requestID: aPageRecord.requestID,
|
||||
requestID: pageRecord.requestID,
|
||||
error: "SecurityError"
|
||||
};
|
||||
mm.sendAsyncMessage("PushService:Register:KO", message);
|
||||
|
@ -890,7 +940,7 @@ this.PushService = {
|
|||
if (!pageRecord.scope || pageRecord.originAttributes === undefined) {
|
||||
debug("Incorrect identifier values set! " + JSON.stringify(pageRecord));
|
||||
let message = {
|
||||
requestID: aPageRecord.requestID,
|
||||
requestID: pageRecord.requestID,
|
||||
error: "SecurityError"
|
||||
};
|
||||
mm.sendAsyncMessage("PushService:Register:KO", message);
|
||||
|
@ -904,8 +954,8 @@ this.PushService = {
|
|||
debug("register(): " + JSON.stringify(aPageRecord));
|
||||
|
||||
this._register(aPageRecord)
|
||||
.then(pushRecord => {
|
||||
let message = this._service.prepareRegister(pushRecord);
|
||||
.then(record => {
|
||||
let message = record.toRegister();
|
||||
message.requestID = aPageRecord.requestID;
|
||||
aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
|
||||
}, error => {
|
||||
|
@ -958,7 +1008,7 @@ this.PushService = {
|
|||
|
||||
return Promise.all([
|
||||
this._sendRequest("unregister", record),
|
||||
this._db.delete(this._service.getKeyFromRecord(record))
|
||||
this._db.delete(record.keyID),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
@ -998,11 +1048,19 @@ this.PushService = {
|
|||
|
||||
return this._checkActivated()
|
||||
.then(_ => this._db.getByIdentifiers(aPageRecord))
|
||||
.then(pushRecord => {
|
||||
if (!pushRecord) {
|
||||
.then(record => {
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return this._service.prepareRegistration(pushRecord);
|
||||
if (record.isExpired()) {
|
||||
return record.getLastVisit().then(lastVisit => {
|
||||
if (lastVisit > record.lastPush) {
|
||||
return this._db.delete(record.keyID).then(_ => null);
|
||||
}
|
||||
throw {state: 0, error: "NotFoundError"};
|
||||
});
|
||||
}
|
||||
return record.toRegistration();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1020,5 +1078,29 @@ this.PushService = {
|
|||
error
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_dropExpiredRegistrations: function() {
|
||||
debug("dropExpiredRegistrations()");
|
||||
|
||||
this._db.getAllExpired().then(records => {
|
||||
return Promise.all(records.map(record => {
|
||||
return record.getLastVisit().then(lastVisit => {
|
||||
if (lastVisit > record.lastPush) {
|
||||
// If the user revisited the site, drop the expired push
|
||||
// registration and notify the associated service worker.
|
||||
return this._db.delete(record.keyID).then(() => {
|
||||
this._notifySubscriptionChangeObservers(record);
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
debug("dropExpiredRegistrations: Error dropping registration " +
|
||||
record.keyID + ": " + error);
|
||||
});
|
||||
}));
|
||||
}).catch(error => {
|
||||
debug("dropExpiredRegistrations: Error dropping registrations: " +
|
||||
error);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ const Cu = Components.utils;
|
|||
const Cr = Components.results;
|
||||
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
|
||||
|
@ -33,7 +34,7 @@ function debug(s) {
|
|||
}
|
||||
|
||||
const kPUSHHTTP2DB_DB_NAME = "pushHttp2";
|
||||
const kPUSHHTTP2DB_DB_VERSION = 3; // Change this if the IndexedDB format changes
|
||||
const kPUSHHTTP2DB_DB_VERSION = 4; // Change this if the IndexedDB format changes
|
||||
const kPUSHHTTP2DB_STORE_NAME = "pushHttp2";
|
||||
|
||||
/**
|
||||
|
@ -285,16 +286,15 @@ SubscriptionListener.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
var reply = {
|
||||
let reply = new PushRecordHttp2({
|
||||
subscriptionUri: subscriptionUri,
|
||||
pushEndpoint: linkParserResult.pushEndpoint,
|
||||
pushReceiptEndpoint: linkParserResult.pushReceiptEndpoint,
|
||||
pageURL: this._subInfo.record.pageURL,
|
||||
scope: this._subInfo.record.scope,
|
||||
originAttributes: this._subInfo.record.originAttributes,
|
||||
pushCount: 0,
|
||||
lastPush: 0
|
||||
};
|
||||
quota: this._subInfo.record.maxQuota,
|
||||
});
|
||||
|
||||
this._subInfo.resolve(reply);
|
||||
},
|
||||
};
|
||||
|
@ -379,51 +379,12 @@ this.PushServiceHttp2 = {
|
|||
_conns: {},
|
||||
_started: false,
|
||||
|
||||
upgradeSchema: function(aTransaction,
|
||||
aDb,
|
||||
aOldVersion,
|
||||
aNewVersion,
|
||||
aDbInstance) {
|
||||
debug("upgradeSchemaHttp2()");
|
||||
|
||||
//XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
|
||||
//registrations away without even informing the app.
|
||||
if (aNewVersion != aOldVersion) {
|
||||
try {
|
||||
aDb.deleteObjectStore(aDbInstance._dbStoreName);
|
||||
} catch (e) {
|
||||
if (e.name === "NotFoundError") {
|
||||
debug("No existing object store found");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let objectStore = aDb.createObjectStore(aDbInstance._dbStoreName,
|
||||
{ keyPath: "subscriptionUri" });
|
||||
|
||||
// index to fetch records based on endpoints. used by unregister
|
||||
objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
|
||||
|
||||
// index to fetch records by identifiers.
|
||||
// In the current security model, the originAttributes distinguish between
|
||||
// different 'apps' on the same origin. Since ServiceWorkers are
|
||||
// same-origin to the scope they are registered for, the attributes and
|
||||
// scope are enough to reconstruct a valid principal.
|
||||
objectStore.createIndex("identifiers", ["scope", "originAttributes"], { unique: true });
|
||||
objectStore.createIndex("originAttributes", "originAttributes", { unique: false });
|
||||
},
|
||||
|
||||
getKeyFromRecord: function(aRecord) {
|
||||
return aRecord.subscriptionUri;
|
||||
},
|
||||
|
||||
newPushDB: function() {
|
||||
return new PushDB(kPUSHHTTP2DB_DB_NAME,
|
||||
kPUSHHTTP2DB_DB_VERSION,
|
||||
kPUSHHTTP2DB_STORE_NAME,
|
||||
this.upgradeSchema);
|
||||
"subscriptionUri",
|
||||
PushRecordHttp2);
|
||||
},
|
||||
|
||||
hasmainPushService: function() {
|
||||
|
@ -798,20 +759,11 @@ this.PushServiceHttp2 = {
|
|||
_pushChannelOnStop: function(aUri, aAckUri, aMessage) {
|
||||
debug("pushChannelOnStop() ");
|
||||
|
||||
let sendNotification = function(aAckUri, aPushRecord, self) {
|
||||
aPushRecord.pushCount = aPushRecord.pushCount + 1;
|
||||
aPushRecord.lastPush = new Date().getTime();
|
||||
self._mainPushService.receivedPushMessage(aPushRecord, aMessage);
|
||||
self._ackMsgRecv(aAckUri);
|
||||
};
|
||||
|
||||
let recoverNoSuchEndpoint = function() {
|
||||
debug("Could not get push endpoint " + aUri + " from DB");
|
||||
};
|
||||
|
||||
this._mainPushService.getByKeyID(aUri)
|
||||
.then(pushRecord => sendNotification(aAckUri, pushRecord, this),
|
||||
recoverNoSuchEndpoint);
|
||||
this._mainPushService.receivedPushMessage(aUri, aMessage, record => {
|
||||
// Always update the stored record.
|
||||
return record;
|
||||
});
|
||||
this._ackMsgRecv(aAckUri);
|
||||
},
|
||||
|
||||
onAlarmFired: function() {
|
||||
|
@ -819,21 +771,30 @@ this.PushServiceHttp2 = {
|
|||
// i.e. when _waitingForPong is true, other conditions are also true.
|
||||
this._startConnectionsWaitingForAlarm();
|
||||
},
|
||||
|
||||
prepareRegistration: function(aPushRecord) {
|
||||
return {
|
||||
pushEndpoint: aPushRecord.pushEndpoint,
|
||||
pushReceiptEndpoint: aPushRecord.pushReceiptEndpoint,
|
||||
version: aPushRecord.version,
|
||||
lastPush: aPushRecord.lastPush,
|
||||
pushCount: aPushRecord.pushCount
|
||||
};
|
||||
},
|
||||
|
||||
prepareRegister: function(aPushRecord) {
|
||||
return {
|
||||
pushEndpoint: aPushRecord.pushEndpoint,
|
||||
pushReceiptEndpoint: aPushRecord.pushReceiptEndpoint
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function PushRecordHttp2(record) {
|
||||
PushRecord.call(this, record);
|
||||
this.subscriptionUri = record.subscriptionUri;
|
||||
this.pushReceiptEndpoint = record.pushReceiptEndpoint;
|
||||
}
|
||||
|
||||
PushRecordHttp2.prototype = Object.create(PushRecord.prototype, {
|
||||
keyID: {
|
||||
get() {
|
||||
return this.subscriptionUri;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
PushRecordHttp2.prototype.toRegistration = function() {
|
||||
let registration = PushRecord.prototype.toRegistration.call(this);
|
||||
registration.pushReceiptEndpoint = this.pushReceiptEndpoint;
|
||||
return registration;
|
||||
};
|
||||
|
||||
PushRecordHttp2.prototype.toRegister = function() {
|
||||
let register = PushRecord.prototype.toRegister.call(this);
|
||||
register.pushReceiptEndpoint = this.pushReceiptEndpoint;
|
||||
return register;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ const Cu = Components.utils;
|
|||
const Cr = Components.results;
|
||||
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
@ -32,7 +33,7 @@ var threadManager = Cc["@mozilla.org/thread-manager;1"]
|
|||
.getService(Ci.nsIThreadManager);
|
||||
|
||||
const kPUSHWSDB_DB_NAME = "pushapi";
|
||||
const kPUSHWSDB_DB_VERSION = 3; // Change this if the IndexedDB format changes
|
||||
const kPUSHWSDB_DB_VERSION = 4; // Change this if the IndexedDB format changes
|
||||
const kPUSHWSDB_STORE_NAME = "pushapi";
|
||||
|
||||
const kUDP_WAKEUP_WS_STATUS_CODE = 4774; // WebSocket Close status code sent
|
||||
|
@ -125,51 +126,12 @@ this.PushServiceWebSocket = {
|
|||
_mainPushService: null,
|
||||
_serverURI: null,
|
||||
|
||||
upgradeSchema: function(aTransaction,
|
||||
aDb,
|
||||
aOldVersion,
|
||||
aNewVersion,
|
||||
aDbInstance) {
|
||||
debug("upgradeSchemaWS()");
|
||||
|
||||
//XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
|
||||
//registrations away without even informing the app.
|
||||
if (aNewVersion != aOldVersion) {
|
||||
try {
|
||||
aDb.deleteObjectStore(aDbInstance._dbStoreName);
|
||||
} catch (e) {
|
||||
if (e.name === "NotFoundError") {
|
||||
debug("No existing object store found");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let objectStore = aDb.createObjectStore(aDbInstance._dbStoreName,
|
||||
{ keyPath: "channelID" });
|
||||
|
||||
// index to fetch records based on endpoints. used by unregister
|
||||
objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
|
||||
|
||||
// index to fetch records by identifiers.
|
||||
// In the current security model, the originAttributes distinguish between
|
||||
// different 'apps' on the same origin. Since ServiceWorkers are
|
||||
// same-origin to the scope they are registered for, the attributes and
|
||||
// scope are enough to reconstruct a valid principal.
|
||||
objectStore.createIndex("identifiers", ["scope", "originAttributes"], { unique: true });
|
||||
objectStore.createIndex("originAttributes", "originAttributes", { unique: false });
|
||||
},
|
||||
|
||||
getKeyFromRecord: function(aRecord) {
|
||||
return aRecord.channelID;
|
||||
},
|
||||
|
||||
newPushDB: function() {
|
||||
return new PushDB(kPUSHWSDB_DB_NAME,
|
||||
kPUSHWSDB_DB_VERSION,
|
||||
kPUSHWSDB_STORE_NAME,
|
||||
this.upgradeSchema);
|
||||
"channelID",
|
||||
PushRecordWebSocket);
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
|
@ -682,10 +644,10 @@ this.PushServiceWebSocket = {
|
|||
}
|
||||
},
|
||||
|
||||
connect: function(channelIDs) {
|
||||
connect: function(records) {
|
||||
debug("connect");
|
||||
// Check to see if we need to do anything.
|
||||
if (channelIDs.length > 0) {
|
||||
if (records.length > 0) {
|
||||
this._beginWSSetup();
|
||||
}
|
||||
},
|
||||
|
@ -883,16 +845,14 @@ this.PushServiceWebSocket = {
|
|||
return;
|
||||
}
|
||||
|
||||
let record = {
|
||||
let record = new PushRecordWebSocket({
|
||||
channelID: reply.channelID,
|
||||
pushEndpoint: reply.pushEndpoint,
|
||||
pageURL: tmp.record.pageURL,
|
||||
scope: tmp.record.scope,
|
||||
originAttributes: tmp.record.originAttributes,
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null
|
||||
};
|
||||
version: null,
|
||||
quota: tmp.record.maxQuota,
|
||||
});
|
||||
dump("PushWebSocket " + JSON.stringify(record));
|
||||
tmp.resolve(record);
|
||||
} else {
|
||||
|
@ -1016,11 +976,10 @@ this.PushServiceWebSocket = {
|
|||
_queueRequest(data) {
|
||||
if (this._currentState != STATE_READY) {
|
||||
if (!this._notifyRequestQueue) {
|
||||
this._enqueue(_ => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._notifyRequestQueue = resolve;
|
||||
});
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
this._notifyRequestQueue = resolve;
|
||||
});
|
||||
this._enqueue(_ => promise);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1041,36 +1000,17 @@ this.PushServiceWebSocket = {
|
|||
_receivedUpdate: function(aChannelID, aLatestVersion) {
|
||||
debug("Updating: " + aChannelID + " -> " + aLatestVersion);
|
||||
|
||||
let compareRecordVersionAndNotify = function(aPushRecord) {
|
||||
debug("compareRecordVersionAndNotify()");
|
||||
if (!aPushRecord) {
|
||||
debug("No record for channel ID " + aChannelID);
|
||||
return;
|
||||
this._mainPushService.receivedPushMessage(aChannelID, "", record => {
|
||||
if (record.version === null ||
|
||||
record.version < aLatestVersion) {
|
||||
debug("Version changed for " + aChannelID + ": " + aLatestVersion);
|
||||
record.version = aLatestVersion;
|
||||
return record;
|
||||
}
|
||||
|
||||
if (aPushRecord.version === null ||
|
||||
aPushRecord.version < aLatestVersion) {
|
||||
debug("Version changed, notifying app and updating DB");
|
||||
aPushRecord.version = aLatestVersion;
|
||||
aPushRecord.pushCount = aPushRecord.pushCount + 1;
|
||||
aPushRecord.lastPush = new Date().getTime();
|
||||
this._mainPushService.receivedPushMessage(aPushRecord,
|
||||
"Short as life is, we make " +
|
||||
"it still shorter by the " +
|
||||
"careless waste of time.");
|
||||
}
|
||||
else {
|
||||
debug("No significant version change: " + aLatestVersion);
|
||||
}
|
||||
};
|
||||
|
||||
let recoverNoSuchChannelID = function(aChannelIDFromServer) {
|
||||
debug("Could not get channelID " + aChannelIDFromServer + " from DB");
|
||||
};
|
||||
|
||||
this._mainPushService.getByKeyID(aChannelID)
|
||||
.then(compareRecordVersionAndNotify.bind(this),
|
||||
err => recoverNoSuchChannelID(err));
|
||||
debug("No significant version change for " + aChannelID + ": " +
|
||||
aLatestVersion);
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
// begin Push protocol handshake
|
||||
|
@ -1121,7 +1061,7 @@ this.PushServiceWebSocket = {
|
|||
};
|
||||
}
|
||||
|
||||
this._mainPushService.getAllKeyIDs()
|
||||
this._mainPushService.getAllUnexpired()
|
||||
.then(sendHelloMessage.bind(this),
|
||||
sendHelloMessage.bind(this));
|
||||
});
|
||||
|
@ -1299,21 +1239,6 @@ this.PushServiceWebSocket = {
|
|||
this._udpServer = undefined;
|
||||
this._beginWSSetup();
|
||||
},
|
||||
|
||||
prepareRegistration: function(aPushRecord) {
|
||||
return {
|
||||
pushEndpoint: aPushRecord.pushEndpoint,
|
||||
version: aPushRecord.version,
|
||||
lastPush: aPushRecord.lastPush,
|
||||
pushCount: aPushRecord.pushCount
|
||||
};
|
||||
},
|
||||
|
||||
prepareRegister: function(aPushRecord) {
|
||||
return {
|
||||
pushEndpoint: aPushRecord.pushEndpoint
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let PushNetworkInfo = {
|
||||
|
@ -1439,3 +1364,23 @@ let PushNetworkInfo = {
|
|||
queryDNSForDomain(netidAddress, callback);
|
||||
}
|
||||
};
|
||||
|
||||
function PushRecordWebSocket(record) {
|
||||
PushRecord.call(this, record);
|
||||
this.channelID = record.channelID;
|
||||
this.version = record.version;
|
||||
}
|
||||
|
||||
PushRecordWebSocket.prototype = Object.create(PushRecord.prototype, {
|
||||
keyID: {
|
||||
get() {
|
||||
return this.channelID;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
PushRecordWebSocket.prototype.toRegistration = function() {
|
||||
let registration = PushRecord.prototype.toRegistration.call(this);
|
||||
registration.version = this.version;
|
||||
return registration;
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ EXTRA_PP_JS_MODULES += [
|
|||
|
||||
EXTRA_JS_MODULES += [
|
||||
'PushDB.jsm',
|
||||
'PushRecord.jsm',
|
||||
'PushService.jsm',
|
||||
'PushServiceHttp2.jsm',
|
||||
]
|
||||
|
|
|
@ -7,8 +7,7 @@ function handlePush(event) {
|
|||
|
||||
self.clients.matchAll().then(function(result) {
|
||||
if (event instanceof PushEvent &&
|
||||
event.data instanceof PushMessageData &&
|
||||
event.data.text().length > 0) {
|
||||
event.data instanceof PushMessageData) {
|
||||
|
||||
result[0].postMessage({type: "finished", okay: "yes"});
|
||||
return;
|
||||
|
|
|
@ -10,6 +10,7 @@ Cu.import('resource://gre/modules/Services.jsm');
|
|||
Cu.import('resource://gre/modules/Timer.jsm');
|
||||
Cu.import('resource://gre/modules/Promise.jsm');
|
||||
Cu.import('resource://gre/modules/Preferences.jsm');
|
||||
Cu.import('resource://gre/modules/PlacesUtils.jsm');
|
||||
|
||||
const serviceExports = Cu.import('resource://gre/modules/PushService.jsm', {});
|
||||
const servicePrefs = new Preferences('dom.push.');
|
||||
|
@ -61,6 +62,25 @@ function after(times, func) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the places database.
|
||||
*
|
||||
* @param {mozIPlaceInfo} place A place record to insert.
|
||||
* @returns {Promise} A promise that fulfills when the database is updated.
|
||||
*/
|
||||
function addVisit(place) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof place.uri == 'string') {
|
||||
place.uri = Services.io.newURI(place.uri, null, null);
|
||||
}
|
||||
PlacesUtils.asyncHistory.updatePlaces(place, {
|
||||
handleCompletion: resolve,
|
||||
handleError: reject,
|
||||
handleResult() {},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers one or more callbacks until the next turn of the event loop. Multiple
|
||||
* callbacks are executed in order.
|
||||
|
@ -199,7 +219,8 @@ function setPrefs(prefs = {}) {
|
|||
'adaptive.mobile': '',
|
||||
'http2.maxRetries': 2,
|
||||
'http2.retryInterval': 500,
|
||||
'http2.reset_retry_count_after_ms': 60000
|
||||
'http2.reset_retry_count_after_ms': 60000,
|
||||
maxQuotaPerSubscription: 16,
|
||||
}, prefs);
|
||||
for (let pref in defaultPrefs) {
|
||||
servicePrefs.set(pref, defaultPrefs[pref]);
|
||||
|
|
|
@ -20,7 +20,9 @@ add_task(function* test_unregister_success() {
|
|||
channelID,
|
||||
pushEndpoint: 'https://example.org/update/unregister-success',
|
||||
scope: 'https://example.com/page/unregister-success',
|
||||
version: 1
|
||||
version: 1,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
});
|
||||
|
||||
let unregisterDefer = Promise.defer();
|
||||
|
|
|
@ -26,19 +26,22 @@ add_task(function* test_notification_ack() {
|
|||
pushEndpoint: 'https://example.com/update/1',
|
||||
scope: 'https://example.org/1',
|
||||
originAttributes: '',
|
||||
version: 1
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
|
||||
pushEndpoint: 'https://example.com/update/2',
|
||||
scope: 'https://example.org/2',
|
||||
originAttributes: '',
|
||||
version: 2
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '5477bfda-22db-45d4-9614-fee369630260',
|
||||
pushEndpoint: 'https://example.com/update/3',
|
||||
scope: 'https://example.org/3',
|
||||
originAttributes: '',
|
||||
version: 3
|
||||
version: 3,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
|
|
|
@ -24,13 +24,15 @@ add_task(function* test_notification_duplicate() {
|
|||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.com/1',
|
||||
originAttributes: "",
|
||||
version: 2
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '27d1e393-03ef-4c72-a5e6-9e890dfccad0',
|
||||
pushEndpoint: 'https://example.org/update/2',
|
||||
scope: 'https://example.com/2',
|
||||
originAttributes: "",
|
||||
version: 2
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
|
|
|
@ -26,19 +26,22 @@ add_task(function* test_notification_error() {
|
|||
pushEndpoint: 'https://example.org/update/success-1',
|
||||
scope: 'https://example.com/a',
|
||||
originAttributes: originAttributes,
|
||||
version: 1
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '3c3930ba-44de-40dc-a7ca-8a133ec1a866',
|
||||
pushEndpoint: 'https://example.org/update/error',
|
||||
scope: 'https://example.com/b',
|
||||
originAttributes: originAttributes,
|
||||
version: 2
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'b63f7bef-0a0d-4236-b41e-086a69dfd316',
|
||||
pushEndpoint: 'https://example.org/update/success-2',
|
||||
scope: 'https://example.com/c',
|
||||
originAttributes: originAttributes,
|
||||
version: 3
|
||||
version: 3,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
|
|
|
@ -58,19 +58,22 @@ add_task(function* test_pushNotifications() {
|
|||
pushEndpoint: serverURL + '/pushEndpoint1',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint1',
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
quota: Infinity,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/pushNotifications/subscription2',
|
||||
pushEndpoint: serverURL + '/pushEndpoint2',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint2',
|
||||
scope: 'https://example.com/page/2',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
quota: Infinity,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/pushNotifications/subscription3',
|
||||
pushEndpoint: serverURL + '/pushEndpoint3',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint3',
|
||||
scope: 'https://example.com/page/3',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
|
|
|
@ -24,25 +24,33 @@ add_task(function* test_notification_incomplete() {
|
|||
channelID: '123',
|
||||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.com/page/1',
|
||||
version: 1
|
||||
version: 1,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '3ad1ed95-d37a-4d88-950f-22cbe2e240d7',
|
||||
pushEndpoint: 'https://example.org/update/2',
|
||||
scope: 'https://example.com/page/2',
|
||||
version: 1
|
||||
version: 1,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'd239498b-1c85-4486-b99b-205866e82d1f',
|
||||
pushEndpoint: 'https://example.org/update/3',
|
||||
scope: 'https://example.com/page/3',
|
||||
version: 3
|
||||
version: 3,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'a50de97d-b496-43ce-8b53-05522feb78db',
|
||||
pushEndpoint: 'https://example.org/update/4',
|
||||
scope: 'https://example.com/page/4',
|
||||
version: 10
|
||||
version: 10,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
db.put(record);
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
Services.obs.addObserver(function observe(subject, topic, data) {
|
||||
|
@ -51,7 +59,7 @@ add_task(function* test_notification_incomplete() {
|
|||
|
||||
let notificationDefer = Promise.defer();
|
||||
let notificationDone = after(2, notificationDefer.resolve);
|
||||
let prevHandler = PushService._handleNotificationReply;
|
||||
let prevHandler = PushServiceWebSocket._handleNotificationReply;
|
||||
PushServiceWebSocket._handleNotificationReply = function _handleNotificationReply() {
|
||||
notificationDone();
|
||||
return prevHandler.apply(this, arguments);
|
||||
|
|
|
@ -22,7 +22,8 @@ add_task(function* test_notification_version_string() {
|
|||
pushEndpoint: 'https://example.org/updates/1',
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: '',
|
||||
version: 2
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
});
|
||||
|
||||
let notifyPromise = promiseObserverNotification('push-notification');
|
||||
|
@ -70,4 +71,5 @@ add_task(function* test_notification_version_string() {
|
|||
let storeRecord = yield db.getByKeyID(
|
||||
'6ff97d56-d0c0-43bc-8f5b-61b855e1d93b');
|
||||
strictEqual(storeRecord.version, 4, 'Wrong record version');
|
||||
equal(storeRecord.quota, Infinity, 'Wrong quota');
|
||||
});
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
const userAgentID = '7eb873f9-8d47-4218-804b-fff78dc04e88';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_expiration_origin_threshold() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => db.drop().then(_ => db.close()));
|
||||
|
||||
yield db.put({
|
||||
channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
|
||||
pushEndpoint: 'https://example.org/push/1',
|
||||
scope: 'https://example.com/auctions',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 16,
|
||||
});
|
||||
yield db.put({
|
||||
channelID: '46cc6f6a-c106-4ffa-bb7c-55c60bd50c41',
|
||||
pushEndpoint: 'https://example.org/push/2',
|
||||
scope: 'https://example.com/deals',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 16,
|
||||
});
|
||||
|
||||
// The notification threshold is per-origin, even with multiple service
|
||||
// workers for different scopes.
|
||||
yield addVisit({
|
||||
uri: 'https://example.com/login',
|
||||
title: 'Sign in to see your auctions',
|
||||
visits: [{
|
||||
visitDate: (Date.now() - 7 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
});
|
||||
|
||||
// We'll always use your most recent visit to an origin.
|
||||
yield addVisit({
|
||||
uri: 'https://example.com/auctions',
|
||||
title: 'Your auctions',
|
||||
visits: [{
|
||||
visitDate: (Date.now() - 2 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
});
|
||||
|
||||
// ...But we won't count downloads or embeds.
|
||||
yield addVisit({
|
||||
uri: 'https://example.com/invoices/invoice.pdf',
|
||||
title: 'Invoice #123',
|
||||
visits: [{
|
||||
visitDate: (Date.now() - 1 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_EMBED,
|
||||
}, {
|
||||
visitDate: Date.now() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
|
||||
}],
|
||||
});
|
||||
|
||||
// We expect to receive 6 notifications: 5 on the `auctions` channel,
|
||||
// and 1 on the `deals` channel. They're from the same origin, but
|
||||
// different scopes, so each can send 5 notifications before we remove
|
||||
// their subscription.
|
||||
let updates = 0;
|
||||
let notifyPromise = promiseObserverNotification('push-notification', (subject, data) => {
|
||||
updates++;
|
||||
return updates == 6;
|
||||
});
|
||||
let unregisterDefer = Promise.defer();
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
networkInfo: new MockDesktopNetworkInfo(),
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
deepEqual(request.channelIDs.sort(), [
|
||||
'46cc6f6a-c106-4ffa-bb7c-55c60bd50c41',
|
||||
'eb33fc90-c883-4267-b5cb-613969e8e349',
|
||||
], 'Wrong active registrations in handshake');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
// We last visited the site 2 days ago, so we can send 5
|
||||
// notifications without throttling. Sending a 6th should
|
||||
// drop the registration.
|
||||
for (let version = 1; version <= 6; version++) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
|
||||
version,
|
||||
}],
|
||||
}));
|
||||
}
|
||||
// But the limits are per-channel, so we can send 5 more
|
||||
// notifications on a different channel.
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: '46cc6f6a-c106-4ffa-bb7c-55c60bd50c41',
|
||||
version: 1,
|
||||
}],
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
equal(request.channelID, 'eb33fc90-c883-4267-b5cb-613969e8e349', 'Unregistered wrong channel ID');
|
||||
unregisterDefer.resolve();
|
||||
},
|
||||
// We expect to receive acks, but don't care about their
|
||||
// contents.
|
||||
onACK(request) {},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
|
||||
'Timed out waiting for unregister request');
|
||||
|
||||
yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
|
||||
'Timed out waiting for notifications');
|
||||
|
||||
let expiredRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349');
|
||||
strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '28cd09e2-7506-42d8-9e50-b02785adc7ef';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_expiration_history_observer() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => db.drop().then(_ => db.close()));
|
||||
|
||||
// A registration that we'll expire...
|
||||
yield db.put({
|
||||
channelID: '379c0668-8323-44d2-a315-4ee83f1a9ee9',
|
||||
pushEndpoint: 'https://example.org/push/1',
|
||||
scope: 'https://example.com/deals',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 16,
|
||||
});
|
||||
|
||||
// ...And an expired registration that we'll revive later.
|
||||
yield db.put({
|
||||
channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
|
||||
pushEndpoint: 'https://example.org/push/2',
|
||||
scope: 'https://example.com/auctions',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 0,
|
||||
});
|
||||
|
||||
yield addVisit({
|
||||
uri: 'https://example.com/infrequent',
|
||||
title: 'Infrequently-visited page',
|
||||
visits: [{
|
||||
visitDate: (Date.now() - 14 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
});
|
||||
|
||||
let unregisterDefer = Promise.defer();
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
networkInfo: new MockDesktopNetworkInfo(),
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
deepEqual(request.channelIDs, [
|
||||
'379c0668-8323-44d2-a315-4ee83f1a9ee9',
|
||||
], 'Should not include expired channel IDs');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: '379c0668-8323-44d2-a315-4ee83f1a9ee9',
|
||||
version: 2,
|
||||
}],
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
equal(request.channelID, '379c0668-8323-44d2-a315-4ee83f1a9ee9', 'Dropped wrong channel ID');
|
||||
unregisterDefer.resolve();
|
||||
},
|
||||
onACK(request) {},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
|
||||
'Timed out waiting for unregister request');
|
||||
|
||||
let expiredRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
|
||||
strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
|
||||
|
||||
let notifiedScopes = [];
|
||||
let subChangePromise = promiseObserverNotification('push-subscription-change', (subject, data) => {
|
||||
notifiedScopes.push(data);
|
||||
return notifiedScopes.length == 2;
|
||||
});
|
||||
|
||||
// Now visit the site...
|
||||
yield addVisit({
|
||||
uri: 'https://example.com/another-page',
|
||||
title: 'Infrequently-visited page',
|
||||
visits: [{
|
||||
visitDate: Date.now() * 1000,
|
||||
transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
}],
|
||||
});
|
||||
Services.obs.notifyObservers(null, 'idle-daily', '');
|
||||
|
||||
// And we should receive notifications for both scopes.
|
||||
yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
|
||||
'Timed out waiting for subscription change events');
|
||||
deepEqual(notifiedScopes.sort(), [
|
||||
'https://example.com/auctions',
|
||||
'https://example.com/deals'
|
||||
], 'Wrong scopes for subscription changes');
|
||||
|
||||
let aRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
|
||||
ok(!aRecord, 'Should drop expired record');
|
||||
|
||||
let bRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349');
|
||||
ok(!bRecord, 'Should drop evicted record');
|
||||
});
|
|
@ -85,7 +85,7 @@ add_task(function* test1() {
|
|||
|
||||
let newRecord = yield PushNotificationService.register(
|
||||
'https://example.com/retry5xxCode',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
);
|
||||
|
||||
var subscriptionUri = serverURL + '/subscription';
|
||||
|
|
|
@ -48,7 +48,7 @@ add_task(function* test_register_case() {
|
|||
|
||||
let newRecord = yield waitForPromise(
|
||||
PushNotificationService.register('https://example.net/case',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
DEFAULT_TIMEOUT,
|
||||
'Mixed-case register response timed out'
|
||||
);
|
||||
|
|
|
@ -30,7 +30,8 @@ add_task(function* test_register_flush() {
|
|||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: '',
|
||||
version: 2
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
};
|
||||
yield db.put(record);
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ add_task(function* test_register_invalid_channel() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.register('https://example.com/invalid-channel',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'Invalid channel ID';
|
||||
},
|
||||
|
|
|
@ -51,7 +51,7 @@ add_task(function* test_register_invalid_endpoint() {
|
|||
yield rejects(
|
||||
PushNotificationService.register(
|
||||
'https://example.net/page/invalid-endpoint',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error && error.includes('Invalid pushEndpoint');
|
||||
},
|
||||
|
|
|
@ -50,7 +50,7 @@ add_task(function* test_register_invalid_json() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.register('https://example.net/page/invalid-json',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'TimeoutError';
|
||||
},
|
||||
|
|
|
@ -54,7 +54,7 @@ add_task(function* test_register_no_id() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.register('https://example.com/incomplete',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'TimeoutError';
|
||||
},
|
||||
|
|
|
@ -46,11 +46,11 @@ add_task(function* test_register_request_queue() {
|
|||
|
||||
let firstRegister = PushNotificationService.register(
|
||||
'https://example.com/page/1',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
);
|
||||
let secondRegister = PushNotificationService.register(
|
||||
'https://example.com/page/1',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
);
|
||||
|
||||
yield waitForPromise(Promise.all([
|
||||
|
|
|
@ -75,7 +75,7 @@ add_task(function* test_register_rollback() {
|
|||
// Should return a rejected promise if storage fails.
|
||||
yield rejects(
|
||||
PushNotificationService.register('https://example.com/storage-error',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'universe has imploded';
|
||||
},
|
||||
|
|
|
@ -58,7 +58,7 @@ add_task(function* test_register_success() {
|
|||
|
||||
let newRecord = yield PushNotificationService.register(
|
||||
'https://example.org/1',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
);
|
||||
equal(newRecord.channelID, channelID,
|
||||
'Wrong channel ID in registration record');
|
||||
|
@ -66,6 +66,8 @@ add_task(function* test_register_success() {
|
|||
'Wrong push endpoint in registration record');
|
||||
equal(newRecord.scope, 'https://example.org/1',
|
||||
'Wrong scope in registration record');
|
||||
equal(newRecord.quota, Infinity,
|
||||
'Wrong quota in registration record');
|
||||
|
||||
let record = yield db.getByKeyID(channelID);
|
||||
equal(record.channelID, channelID,
|
||||
|
@ -74,4 +76,6 @@ add_task(function* test_register_success() {
|
|||
'Wrong push endpoint in database record');
|
||||
equal(record.scope, 'https://example.org/1',
|
||||
'Wrong scope in database record');
|
||||
equal(record.quota, Infinity,
|
||||
'Wrong quota in database record');
|
||||
});
|
||||
|
|
|
@ -84,7 +84,7 @@ add_task(function* test_register_timeout() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.register('https://example.net/page/timeout',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'TimeoutError';
|
||||
},
|
||||
|
|
|
@ -60,7 +60,7 @@ add_task(function* test_register_wrong_id() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.register('https://example.com/mismatched',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'TimeoutError';
|
||||
},
|
||||
|
|
|
@ -56,7 +56,7 @@ add_task(function* test_register_wrong_type() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.register('https://example.com/mistyped',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'TimeoutError';
|
||||
},
|
||||
|
|
|
@ -32,7 +32,7 @@ add_task(function* test_registrations_error() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.registration('https://example.net/1',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error == 'Database error';
|
||||
},
|
||||
|
|
|
@ -25,6 +25,6 @@ add_task(function* test_registration_none() {
|
|||
|
||||
let registration = yield PushNotificationService.registration(
|
||||
'https://example.net/1',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false });
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }));
|
||||
ok(!registration, 'Should not open a connection without registration');
|
||||
});
|
||||
|
|
|
@ -21,24 +21,28 @@ add_task(function* test_registration_success() {
|
|||
pushEndpoint: 'https://example.com/update/same-manifest/1',
|
||||
scope: 'https://example.net/a',
|
||||
originAttributes: '',
|
||||
version: 5
|
||||
version: 5,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'f6edfbcd-79d6-49b8-9766-48b9dcfeff0f',
|
||||
pushEndpoint: 'https://example.com/update/same-manifest/2',
|
||||
scope: 'https://example.net/b',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: 42 }),
|
||||
version: 10
|
||||
version: 10,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'b1cf38c9-6836-4d29-8a30-a3e98d59b728',
|
||||
pushEndpoint: 'https://example.org/update/different-manifest',
|
||||
scope: 'https://example.org/c',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: 42, inBrowser: true }),
|
||||
version: 15
|
||||
version: 15,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
let handshakeDefer = Promise.defer();
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
networkInfo: new MockDesktopNetworkInfo(),
|
||||
|
@ -56,11 +60,18 @@ add_task(function* test_registration_success() {
|
|||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
handshakeDefer.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield waitForPromise(
|
||||
handshakeDefer.promise,
|
||||
DEFAULT_TIMEOUT,
|
||||
'Timed out waiting for handshake'
|
||||
);
|
||||
|
||||
let registration = yield PushNotificationService.registration(
|
||||
'https://example.net/a', '');
|
||||
equal(
|
||||
|
|
|
@ -42,19 +42,22 @@ add_task(function* test_pushNotifications() {
|
|||
pushEndpoint: serverURL + '/pushEndpointA',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpointA',
|
||||
scope: 'https://example.net/a',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
quota: Infinity,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/subscriptionB',
|
||||
pushEndpoint: serverURL + '/pushEndpointB',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpointB',
|
||||
scope: 'https://example.net/b',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
quota: Infinity,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/subscriptionC',
|
||||
pushEndpoint: serverURL + '/pushEndpointC',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpointC',
|
||||
scope: 'https://example.net/c',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
|
|
|
@ -72,7 +72,9 @@ add_task(function* test1() {
|
|||
subscriptionUri: serverURL + '/subscription4xxCode',
|
||||
pushEndpoint: serverURL + '/pushEndpoint',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
|
||||
scope: 'https://example.com/page'
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
|
|
|
@ -82,7 +82,9 @@ add_task(function* test1() {
|
|||
subscriptionUri: serverURL + '/subscription5xxCode',
|
||||
pushEndpoint: serverURL + '/pushEndpoint',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
|
||||
scope: 'https://example.com/page'
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
|
|
|
@ -67,7 +67,9 @@ add_task(function* test1() {
|
|||
subscriptionUri: 'http://localhost/subscriptionNotExist',
|
||||
pushEndpoint: serverURL + '/pushEndpoint',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
|
||||
scope: 'https://example.com/page'
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
|
|
|
@ -30,7 +30,7 @@ add_task(function* test_unregister_empty_scope() {
|
|||
|
||||
yield rejects(
|
||||
PushNotificationService.unregister('',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })),
|
||||
function(error) {
|
||||
return error.error == 'NotFoundError';
|
||||
},
|
||||
|
|
|
@ -21,7 +21,8 @@ add_task(function* test_unregister_error() {
|
|||
pushEndpoint: 'https://example.org/update/failure',
|
||||
scope: 'https://example.net/page/failure',
|
||||
originAttributes: '',
|
||||
version: 1
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
});
|
||||
|
||||
let unregisterDefer = Promise.defer();
|
||||
|
|
|
@ -25,13 +25,15 @@ add_task(function* test_unregister_invalid_json() {
|
|||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.edu/page/1',
|
||||
originAttributes: '',
|
||||
version: 1
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '057caa8f-9b99-47ff-891c-adad18ce603e',
|
||||
pushEndpoint: 'https://example.com/update/2',
|
||||
scope: 'https://example.net/page/1',
|
||||
originAttributes: '',
|
||||
version: 1
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
|
|
|
@ -30,7 +30,7 @@ add_task(function* test_unregister_not_found() {
|
|||
|
||||
let promise = PushNotificationService.unregister(
|
||||
'https://example.net/nonexistent',
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false });
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }));
|
||||
yield rejects(promise, function(error) {
|
||||
return error == 'NotFoundError';
|
||||
}, 'Wrong error for nonexistent scope');
|
||||
|
|
|
@ -21,7 +21,8 @@ add_task(function* test_unregister_success() {
|
|||
pushEndpoint: 'https://example.org/update/unregister-success',
|
||||
scope: 'https://example.com/page/unregister-success',
|
||||
originAttributes: '',
|
||||
version: 1
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
});
|
||||
|
||||
let unregisterDefer = Promise.defer();
|
||||
|
|
|
@ -55,6 +55,7 @@ add_task(function* test_pushUnsubscriptionSuccess() {
|
|||
pushReceiptEndpoint: serverURL + '/receiptPushEndpointUnsubscriptionSuccess',
|
||||
scope: 'https://example.com/page/unregister-success',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
|
||||
quota: Infinity,
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
|
|
|
@ -26,7 +26,6 @@ add_task(function* test_webapps_cleardata() {
|
|||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushService._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org",
|
||||
networkInfo: new MockDesktopNetworkInfo(),
|
||||
|
|
|
@ -9,6 +9,8 @@ skip-if = toolkit == 'android'
|
|||
[test_notification_error.js]
|
||||
[test_notification_incomplete.js]
|
||||
[test_notification_version_string.js]
|
||||
[test_quota_exceeded.js]
|
||||
[test_quota_observer.js]
|
||||
[test_register_case.js]
|
||||
[test_register_flush.js]
|
||||
[test_register_invalid_channel.js]
|
||||
|
|
|
@ -4409,6 +4409,10 @@ pref("dom.push.debug", false);
|
|||
pref("dom.push.serverURL", "wss://push.services.mozilla.com/");
|
||||
pref("dom.push.userAgentID", "");
|
||||
|
||||
// The maximum number of notifications that a service worker can receive
|
||||
// without user interaction.
|
||||
pref("dom.push.maxQuotaPerSubscription", 16);
|
||||
|
||||
// Is the network connection allowed to be up?
|
||||
// This preference should be used in UX to enable/disable push.
|
||||
pref("dom.push.connection.enabled", true);
|
||||
|
|
Загрузка…
Ссылка в новой задаче