зеркало из https://github.com/mozilla/gecko-dev.git
484 строки
13 KiB
JavaScript
484 строки
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* This file exports XPCOM components for C++ and chrome JavaScript callers to
|
|
* interact with the Push service.
|
|
*/
|
|
|
|
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
|
|
|
/**
|
|
* `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.jsm` 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: XPCOMUtils.generateQI([
|
|
Ci.nsIObserver,
|
|
Ci.nsISupportsWeakReference,
|
|
Ci.nsIPushService,
|
|
Ci.nsIPushQuotaManager,
|
|
]),
|
|
|
|
_handleReady() {},
|
|
|
|
_addListeners() {
|
|
for (let message of this._messages) {
|
|
this._mm.addMessageListener(message, this);
|
|
}
|
|
},
|
|
|
|
_isValidMessage(message) {
|
|
return this._messages.includes(message.name);
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic === "app-startup") {
|
|
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
|
|
return;
|
|
}
|
|
if (topic === "sessionstore-windows-restored") {
|
|
Services.obs.removeObserver(this, "sessionstore-windows-restored");
|
|
this._handleReady();
|
|
return;
|
|
}
|
|
},
|
|
|
|
_deliverSubscription(request, props) {
|
|
if (!props) {
|
|
request.onPushSubscription(Cr.NS_OK, null);
|
|
return;
|
|
}
|
|
request.onPushSubscription(Cr.NS_OK, new PushSubscription(props));
|
|
},
|
|
};
|
|
|
|
/**
|
|
* The parent process implementation of `nsIPushService`. This version loads
|
|
* `PushService.jsm` at startup and calls its methods directly. It also
|
|
* receives and responds to requests from the content process.
|
|
*/
|
|
function PushServiceParent() {
|
|
PushServiceBase.call(this);
|
|
}
|
|
|
|
PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(PushServiceParent.prototype, "_mm",
|
|
"@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
|
|
|
|
XPCOMUtils.defineLazyGetter(PushServiceParent.prototype, "_service",
|
|
function() {
|
|
const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
|
|
{});
|
|
PushService.init();
|
|
return PushService;
|
|
});
|
|
|
|
Object.assign(PushServiceParent.prototype, {
|
|
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent),
|
|
|
|
_messages: [
|
|
"Push:Register",
|
|
"Push:Registration",
|
|
"Push:Unregister",
|
|
"Push:Clear",
|
|
"Push:RegisterEventNotificationListener",
|
|
"Push:NotificationForOriginShown",
|
|
"Push:NotificationForOriginClosed",
|
|
"child-process-shutdown",
|
|
],
|
|
|
|
// nsIPushService methods
|
|
|
|
subscribe(scope, principal, callback) {
|
|
return this._handleRequest("Push:Register", principal, {
|
|
scope: scope,
|
|
}).then(result => {
|
|
this._deliverSubscription(callback, result);
|
|
}, error => {
|
|
callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
|
|
}).catch(Cu.reportError);
|
|
},
|
|
|
|
unsubscribe(scope, principal, callback) {
|
|
this._handleRequest("Push:Unregister", principal, {
|
|
scope: scope,
|
|
}).then(result => {
|
|
callback.onUnsubscribe(Cr.NS_OK, result);
|
|
}, error => {
|
|
callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
|
|
}).catch(Cu.reportError);
|
|
},
|
|
|
|
getSubscription(scope, principal, callback) {
|
|
return this._handleRequest("Push:Registration", principal, {
|
|
scope: scope,
|
|
}).then(result => {
|
|
this._deliverSubscription(callback, result);
|
|
}, error => {
|
|
callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
|
|
}).catch(Cu.reportError);
|
|
},
|
|
|
|
clearForDomain(domain, callback) {
|
|
let principal = Services.scriptSecurityManager.getSystemPrincipal();
|
|
return this._handleRequest("Push:Clear", principal, {
|
|
domain: domain,
|
|
}).then(result => {
|
|
callback.onClear(Cr.NS_OK);
|
|
}, error => {
|
|
callback.onClear(Cr.NS_ERROR_FAILURE);
|
|
}).catch(Cu.reportError);
|
|
},
|
|
|
|
// nsIPushQuotaManager methods
|
|
|
|
notificationForOriginShown(origin) {
|
|
this._service.notificationForOriginShown(origin);
|
|
},
|
|
|
|
notificationForOriginClosed(origin) {
|
|
this._service.notificationForOriginClosed(origin);
|
|
},
|
|
|
|
receiveMessage(message) {
|
|
if (!this._isValidMessage(message)) {
|
|
return;
|
|
}
|
|
let {name, principal, target, data} = message;
|
|
if (name === "Push:RegisterEventNotificationListener") {
|
|
this._service.registerListener(target);
|
|
return;
|
|
}
|
|
if (name === "child-process-shutdown") {
|
|
this._service.unregisterListener(target);
|
|
return;
|
|
}
|
|
if (name === "Push:NotificationForOriginShown") {
|
|
this.notificationForOriginShown(data);
|
|
return;
|
|
}
|
|
if (name === "Push:NotificationForOriginClosed") {
|
|
this.notificationForOriginClosed(data);
|
|
return;
|
|
}
|
|
if (!target.assertPermission("push")) {
|
|
return;
|
|
}
|
|
let sender = target.QueryInterface(Ci.nsIMessageSender);
|
|
return this._handleRequest(name, principal, data).then(result => {
|
|
sender.sendAsyncMessage(this._getResponseName(name, "OK"), {
|
|
requestID: data.requestID,
|
|
result: result
|
|
});
|
|
}, error => {
|
|
sender.sendAsyncMessage(this._getResponseName(name, "KO"), {
|
|
requestID: data.requestID,
|
|
});
|
|
}).catch(Cu.reportError);
|
|
},
|
|
|
|
_handleReady() {
|
|
this._service.init();
|
|
},
|
|
|
|
_toPageRecord(principal, data) {
|
|
if (!data.scope) {
|
|
throw new Error("Invalid page record: missing scope");
|
|
}
|
|
|
|
data.originAttributes =
|
|
ChromeUtils.originAttributesToSuffix(principal.originAttributes);
|
|
|
|
return data;
|
|
},
|
|
|
|
_handleRequest(name, principal, data) {
|
|
if (!principal) {
|
|
return Promise.reject(new Error("Invalid request: missing principal"));
|
|
}
|
|
|
|
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;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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() {
|
|
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",
|
|
"nsISyncMessageSender");
|
|
|
|
Object.assign(PushServiceContent.prototype, {
|
|
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceContent),
|
|
|
|
_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) {
|
|
let requestId = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Register", {
|
|
scope: scope,
|
|
requestID: requestId,
|
|
}, null, principal);
|
|
},
|
|
|
|
unsubscribe(scope, principal, callback) {
|
|
let requestId = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Unregister", {
|
|
scope: scope,
|
|
requestID: requestId,
|
|
}, null, principal);
|
|
},
|
|
|
|
getSubscription(scope, principal, callback) {
|
|
let requestId = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Registration", {
|
|
scope: scope,
|
|
requestID: requestId,
|
|
}, null, principal);
|
|
},
|
|
|
|
clearForDomain(domain, callback) {
|
|
let requestId = this._addRequest(callback);
|
|
this._mm.sendAsyncMessage("Push:Clear", {
|
|
domain: domain,
|
|
requestID: requestId,
|
|
});
|
|
},
|
|
|
|
// nsIPushQuotaManager methods
|
|
|
|
notificationForOriginShown(origin) {
|
|
this._mm.sendAsyncMessage("Push:NotificationForOriginShown", origin);
|
|
},
|
|
|
|
notificationForOriginClosed(origin) {
|
|
this._mm.sendAsyncMessage("Push:NotificationForOriginClosed", origin);
|
|
},
|
|
|
|
_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":
|
|
request.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
|
|
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: XPCOMUtils.generateQI([Ci.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 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, outKeyLen) {
|
|
if (name === "p256dh") {
|
|
return this._getRawKey(this._props.p256dhKey, outKeyLen);
|
|
}
|
|
if (name === "auth") {
|
|
return this._getRawKey(this._props.authenticationSecret, outKeyLen);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
_getRawKey(key, outKeyLen) {
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
let rawKey = new Uint8Array(key);
|
|
if (outKeyLen) {
|
|
outKeyLen.value = rawKey.length;
|
|
}
|
|
return rawKey;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* `PushObserverNotification` instances are passed to all
|
|
* `push-notification` observers.
|
|
*/
|
|
function PushObserverNotification() {}
|
|
|
|
PushObserverNotification.prototype = {
|
|
classID: Components.ID("{e68997fd-8b92-49ee-af12-800830b023e8}"),
|
|
contractID: "@mozilla.org/push/ObserverNotification;1",
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushObserverNotification]),
|
|
};
|
|
|
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
|
|
PushObserverNotification,
|
|
|
|
// Export the correct implementation depending on whether we're running in
|
|
// the parent or content process.
|
|
isParent ? PushServiceParent : PushServiceContent,
|
|
]);
|