mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
0453db814c
Differential Revision: https://phabricator.services.mozilla.com/D204183
586 lines
15 KiB
JavaScript
586 lines
15 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/. */
|
|
|
|
/**
|
|
* This file exports XPCOM components for C++ and chrome JavaScript callers to
|
|
* interact with the Push service.
|
|
*/
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
var isParent =
|
|
Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
|
|
|
const lazy = {};
|
|
|
|
// The default Push service implementation.
|
|
ChromeUtils.defineLazyGetter(lazy, "PushService", function () {
|
|
if (Services.prefs.getBoolPref("dom.push.enabled")) {
|
|
const { PushService } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/PushService.sys.mjs"
|
|
);
|
|
PushService.init();
|
|
return PushService;
|
|
}
|
|
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
|
|
});
|
|
|
|
// Observer notification topics for push messages and subscription status
|
|
// changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
|
|
// on `nsIPushService` so that JS callers only need to import this service.
|
|
const OBSERVER_TOPIC_PUSH = "push-message";
|
|
const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
|
|
const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";
|
|
|
|
/**
|
|
* `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
|
|
* implement the `nsIPushService` interface. This interface provides calls
|
|
* similar to the Push DOM API, but does not require service workers.
|
|
*
|
|
* Push service methods may be called from the parent or content process. The
|
|
* parent process implementation loads `PushService.sys.mjs` at app startup, and
|
|
* calls its methods directly. The content implementation forwards calls to
|
|
* the parent Push service via IPC.
|
|
*
|
|
* The implementations share a class and contract ID.
|
|
*/
|
|
function PushServiceBase() {
|
|
this.wrappedJSObject = this;
|
|
this._addListeners();
|
|
}
|
|
|
|
PushServiceBase.prototype = {
|
|
classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"),
|
|
contractID: "@mozilla.org/push/Service;1",
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsISupportsWeakReference",
|
|
"nsIPushService",
|
|
"nsIPushQuotaManager",
|
|
"nsIPushErrorReporter",
|
|
]),
|
|
|
|
pushTopic: OBSERVER_TOPIC_PUSH,
|
|
subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
|
|
subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
|
|
|
|
ensureReady() {},
|
|
|
|
_addListeners() {
|
|
for (let message of this._messages) {
|
|
this._mm.addMessageListener(message, this);
|
|
}
|
|
},
|
|
|
|
_isValidMessage(message) {
|
|
return this._messages.includes(message.name);
|
|
},
|
|
|
|
observe(subject, topic) {
|
|
if (topic === "android-push-service") {
|
|
// Load PushService immediately.
|
|
this.ensureReady();
|
|
}
|
|
},
|
|
|
|
_deliverSubscription(request, props) {
|
|
if (!props) {
|
|
request.onPushSubscription(Cr.NS_OK, null);
|
|
return;
|
|
}
|
|
request.onPushSubscription(Cr.NS_OK, new PushSubscription(props));
|
|
},
|
|
|
|
_deliverSubscriptionError(request, error) {
|
|
let result =
|
|
typeof error.result == "number" ? error.result : Cr.NS_ERROR_FAILURE;
|
|
request.onPushSubscription(result, null);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* The parent process implementation of `nsIPushService`. This version loads
|
|
* `PushService.sys.mjs` at startup and calls its methods directly. It also
|
|
* receives and responds to requests from the content process.
|
|
*/
|
|
let parentInstance;
|
|
function PushServiceParent() {
|
|
if (parentInstance) {
|
|
return parentInstance;
|
|
}
|
|
parentInstance = this;
|
|
|
|
PushServiceBase.call(this);
|
|
}
|
|
|
|
PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
PushServiceParent.prototype,
|
|
"_mm",
|
|
"@mozilla.org/parentprocessmessagemanager;1",
|
|
"nsISupports"
|
|
);
|
|
|
|
Object.assign(PushServiceParent.prototype, {
|
|
_messages: [
|
|
"Push:Register",
|
|
"Push:Registration",
|
|
"Push:Unregister",
|
|
"Push:Clear",
|
|
"Push:NotificationForOriginShown",
|
|
"Push:NotificationForOriginClosed",
|
|
"Push:ReportError",
|
|
],
|
|
|
|
// nsIPushService methods
|
|
|
|
subscribe(scope, principal, callback) {
|
|
this.subscribeWithKey(scope, principal, [], callback);
|
|
},
|
|
|
|
subscribeWithKey(scope, principal, key, callback) {
|
|
this._handleRequest("Push:Register", principal, {
|
|
scope,
|
|
appServerKey: key,
|
|
})
|
|
.then(
|
|
result => {
|
|
this._deliverSubscription(callback, result);
|
|
},
|
|
error => {
|
|
this._deliverSubscriptionError(callback, error);
|
|
}
|
|
)
|
|
.catch(console.error);
|
|
},
|
|
|
|
unsubscribe(scope, principal, callback) {
|
|
this._handleRequest("Push:Unregister", principal, {
|
|
scope,
|
|
})
|
|
.then(
|
|
result => {
|
|
callback.onUnsubscribe(Cr.NS_OK, result);
|
|
},
|
|
() => {
|
|
callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
|
|
}
|
|
)
|
|
.catch(console.error);
|
|
},
|
|
|
|
getSubscription(scope, principal, callback) {
|
|
return this._handleRequest("Push:Registration", principal, {
|
|
scope,
|
|
})
|
|
.then(
|
|
result => {
|
|
this._deliverSubscription(callback, result);
|
|
},
|
|
error => {
|
|
this._deliverSubscriptionError(callback, error);
|
|
}
|
|
)
|
|
.catch(console.error);
|
|
},
|
|
|
|
clearForDomain(domain, callback) {
|
|
return this._handleRequest("Push:Clear", null, {
|
|
domain,
|
|
})
|
|
.then(
|
|
() => {
|
|
callback.onClear(Cr.NS_OK);
|
|
},
|
|
() => {
|
|
callback.onClear(Cr.NS_ERROR_FAILURE);
|
|
}
|
|
)
|
|
.catch(console.error);
|
|
},
|
|
|
|
// nsIPushQuotaManager methods
|
|
|
|
notificationForOriginShown(origin) {
|
|
this.service.notificationForOriginShown(origin);
|
|
},
|
|
|
|
notificationForOriginClosed(origin) {
|
|
this.service.notificationForOriginClosed(origin);
|
|
},
|
|
|
|
// nsIPushErrorReporter methods
|
|
|
|
reportDeliveryError(messageId, reason) {
|
|
this.service.reportDeliveryError(messageId, reason);
|
|
},
|
|
|
|
receiveMessage(message) {
|
|
if (!this._isValidMessage(message)) {
|
|
return;
|
|
}
|
|
let { name, target, data } = message;
|
|
if (name === "Push:NotificationForOriginShown") {
|
|
this.notificationForOriginShown(data);
|
|
return;
|
|
}
|
|
if (name === "Push:NotificationForOriginClosed") {
|
|
this.notificationForOriginClosed(data);
|
|
return;
|
|
}
|
|
if (name === "Push:ReportError") {
|
|
this.reportDeliveryError(data.messageId, data.reason);
|
|
return;
|
|
}
|
|
this._handleRequest(name, data.principal, data)
|
|
.then(
|
|
result => {
|
|
target.sendAsyncMessage(this._getResponseName(name, "OK"), {
|
|
requestID: data.requestID,
|
|
result,
|
|
});
|
|
},
|
|
error => {
|
|
target.sendAsyncMessage(this._getResponseName(name, "KO"), {
|
|
requestID: data.requestID,
|
|
result: error.result,
|
|
});
|
|
}
|
|
)
|
|
.catch(console.error);
|
|
},
|
|
|
|
ensureReady() {
|
|
this.service.init();
|
|
},
|
|
|
|
_toPageRecord(principal, data) {
|
|
if (!data.scope) {
|
|
throw new Error("Invalid page record: missing scope");
|
|
}
|
|
if (!principal) {
|
|
throw new Error("Invalid page record: missing principal");
|
|
}
|
|
if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
|
|
throw new Error("Invalid page record: unsupported principal");
|
|
}
|
|
|
|
// System subscriptions can only be created by chrome callers, and are
|
|
// exempt from the background message quota and permission checks. They
|
|
// also do not fire service worker events.
|
|
data.systemRecord = principal.isSystemPrincipal;
|
|
|
|
data.originAttributes = ChromeUtils.originAttributesToSuffix(
|
|
principal.originAttributes
|
|
);
|
|
|
|
return data;
|
|
},
|
|
|
|
async _handleRequest(name, principal, data) {
|
|
if (name == "Push:Clear") {
|
|
return this.service.clear(data);
|
|
}
|
|
|
|
let pageRecord;
|
|
try {
|
|
pageRecord = this._toPageRecord(principal, data);
|
|
} catch (e) {
|
|
return Promise.reject(e);
|
|
}
|
|
|
|
if (name === "Push:Register") {
|
|
return this.service.register(pageRecord);
|
|
}
|
|
if (name === "Push:Registration") {
|
|
return this.service.registration(pageRecord);
|
|
}
|
|
if (name === "Push:Unregister") {
|
|
return this.service.unregister(pageRecord);
|
|
}
|
|
|
|
return Promise.reject(new Error("Invalid request: unknown name"));
|
|
},
|
|
|
|
_getResponseName(requestName, suffix) {
|
|
let name = requestName.slice("Push:".length);
|
|
return "PushService:" + name + ":" + suffix;
|
|
},
|
|
|
|
// Methods used for mocking in tests.
|
|
|
|
replaceServiceBackend(options) {
|
|
return this.service.changeTestServer(options.serverURI, options);
|
|
},
|
|
|
|
restoreServiceBackend() {
|
|
var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
|
|
return this.service.changeTestServer(defaultServerURL);
|
|
},
|
|
});
|
|
|
|
// Used to replace the implementation with a mock.
|
|
Object.defineProperty(PushServiceParent.prototype, "service", {
|
|
get() {
|
|
return this._service || lazy.PushService;
|
|
},
|
|
set(impl) {
|
|
this._service = impl;
|
|
},
|
|
});
|
|
|
|
let contentInstance;
|
|
/**
|
|
* The content process implementation of `nsIPushService`. This version
|
|
* uses the child message manager to forward calls to the parent process.
|
|
* The parent Push service instance handles the request, and responds with a
|
|
* message containing the result.
|
|
*/
|
|
function PushServiceContent() {
|
|
if (contentInstance) {
|
|
return contentInstance;
|
|
}
|
|
contentInstance = this;
|
|
|
|
PushServiceBase.apply(this, arguments);
|
|
this._requests = new Map();
|
|
this._requestId = 0;
|
|
}
|
|
|
|
PushServiceContent.prototype = Object.create(PushServiceBase.prototype);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
PushServiceContent.prototype,
|
|
"_mm",
|
|
"@mozilla.org/childprocessmessagemanager;1",
|
|
"nsISupports"
|
|
);
|
|
|
|
Object.assign(PushServiceContent.prototype, {
|
|
_messages: [
|
|
"PushService:Register:OK",
|
|
"PushService:Register:KO",
|
|
"PushService:Registration:OK",
|
|
"PushService:Registration:KO",
|
|
"PushService:Unregister:OK",
|
|
"PushService:Unregister:KO",
|
|
"PushService:Clear:OK",
|
|
"PushService:Clear:KO",
|
|
],
|
|
|
|
// nsIPushService methods
|
|
|
|
subscribe(scope, principal, callback) {
|
|
this.subscribeWithKey(scope, principal, [], callback);
|
|
},
|
|
|
|
subscribeWithKey(scope, principal, key, callback) {
|
|
let requestID = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Register", {
|
|
scope,
|
|
appServerKey: key,
|
|
requestID,
|
|
principal,
|
|
});
|
|
},
|
|
|
|
unsubscribe(scope, principal, callback) {
|
|
let requestID = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Unregister", {
|
|
scope,
|
|
requestID,
|
|
principal,
|
|
});
|
|
},
|
|
|
|
getSubscription(scope, principal, callback) {
|
|
let requestID = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Registration", {
|
|
scope,
|
|
requestID,
|
|
principal,
|
|
});
|
|
},
|
|
|
|
clearForDomain(domain, callback) {
|
|
let requestID = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Clear", {
|
|
domain,
|
|
requestID,
|
|
});
|
|
},
|
|
|
|
// nsIPushQuotaManager methods
|
|
|
|
notificationForOriginShown(origin) {
|
|
this._mm.sendAsyncMessage("Push:NotificationForOriginShown", origin);
|
|
},
|
|
|
|
notificationForOriginClosed(origin) {
|
|
this._mm.sendAsyncMessage("Push:NotificationForOriginClosed", origin);
|
|
},
|
|
|
|
// nsIPushErrorReporter methods
|
|
|
|
reportDeliveryError(messageId, reason) {
|
|
this._mm.sendAsyncMessage("Push:ReportError", {
|
|
messageId,
|
|
reason,
|
|
});
|
|
},
|
|
|
|
_addRequest(data) {
|
|
let id = ++this._requestId;
|
|
this._requests.set(id, data);
|
|
return id;
|
|
},
|
|
|
|
_takeRequest(requestId) {
|
|
let d = this._requests.get(requestId);
|
|
this._requests.delete(requestId);
|
|
return d;
|
|
},
|
|
|
|
receiveMessage(message) {
|
|
if (!this._isValidMessage(message)) {
|
|
return;
|
|
}
|
|
let { name, data } = message;
|
|
let request = this._takeRequest(data.requestID);
|
|
|
|
if (!request) {
|
|
return;
|
|
}
|
|
|
|
switch (name) {
|
|
case "PushService:Register:OK":
|
|
case "PushService:Registration:OK":
|
|
this._deliverSubscription(request, data.result);
|
|
break;
|
|
|
|
case "PushService:Register:KO":
|
|
case "PushService:Registration:KO":
|
|
this._deliverSubscriptionError(request, data);
|
|
break;
|
|
|
|
case "PushService:Unregister:OK":
|
|
if (typeof data.result === "boolean") {
|
|
request.onUnsubscribe(Cr.NS_OK, data.result);
|
|
} else {
|
|
request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
|
|
}
|
|
break;
|
|
|
|
case "PushService:Unregister:KO":
|
|
request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
|
|
break;
|
|
|
|
case "PushService:Clear:OK":
|
|
request.onClear(Cr.NS_OK);
|
|
break;
|
|
|
|
case "PushService:Clear:KO":
|
|
request.onClear(Cr.NS_ERROR_FAILURE);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
});
|
|
|
|
/** `PushSubscription` instances are passed to all subscription callbacks. */
|
|
function PushSubscription(props) {
|
|
this._props = props;
|
|
}
|
|
|
|
PushSubscription.prototype = {
|
|
QueryInterface: ChromeUtils.generateQI(["nsIPushSubscription"]),
|
|
|
|
/** The URL for sending messages to this subscription. */
|
|
get endpoint() {
|
|
return this._props.endpoint;
|
|
},
|
|
|
|
/** The last time a message was sent to this subscription. */
|
|
get lastPush() {
|
|
return this._props.lastPush;
|
|
},
|
|
|
|
/** The total number of messages sent to this subscription. */
|
|
get pushCount() {
|
|
return this._props.pushCount;
|
|
},
|
|
|
|
/** The number of remaining background messages that can be sent to this
|
|
* subscription, or -1 of the subscription is exempt from the quota.
|
|
*/
|
|
get quota() {
|
|
return this._props.quota;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether this subscription was created with the system principal.
|
|
* System subscriptions are exempt from the background message quota and
|
|
* permission checks.
|
|
*/
|
|
get isSystemSubscription() {
|
|
return !!this._props.systemRecord;
|
|
},
|
|
|
|
/** The private key used to decrypt incoming push messages, in JWK format */
|
|
get p256dhPrivateKey() {
|
|
return this._props.p256dhPrivateKey;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether this subscription is subject to the background message
|
|
* quota.
|
|
*/
|
|
quotaApplies() {
|
|
return this.quota >= 0;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether this subscription exceeded the background message quota,
|
|
* or the user revoked the notification permission. The caller must request a
|
|
* new subscription to continue receiving push messages.
|
|
*/
|
|
isExpired() {
|
|
return this.quota === 0;
|
|
},
|
|
|
|
/**
|
|
* Returns a key for encrypting messages sent to this subscription. JS
|
|
* callers receive the key buffer as a return value, while C++ callers
|
|
* receive the key size and buffer as out parameters.
|
|
*/
|
|
getKey(name) {
|
|
switch (name) {
|
|
case "p256dh":
|
|
return this._getRawKey(this._props.p256dhKey);
|
|
|
|
case "auth":
|
|
return this._getRawKey(this._props.authenticationSecret);
|
|
|
|
case "appServer":
|
|
return this._getRawKey(this._props.appServerKey);
|
|
}
|
|
return [];
|
|
},
|
|
|
|
_getRawKey(key) {
|
|
if (!key) {
|
|
return [];
|
|
}
|
|
return new Uint8Array(key);
|
|
},
|
|
};
|
|
|
|
// Export the correct implementation depending on whether we're running in
|
|
// the parent or content process.
|
|
export let Service = isParent ? PushServiceParent : PushServiceContent;
|