gecko-dev/dom/push/PushComponents.js
Sebastian Hengst 3a10644021 Backed out 6 changesets (bug 888600) for beta simulation failures: build bustage on Linux and Windows opt (bug 1442036) and devtools failure browser_net_view-source-debugger.js (bug 1441961). a=backout
Backed out changeset 83c87140dc3d (bug 888600)
Backed out changeset 2efb9b1753f6 (bug 888600)
Backed out changeset af5303781961 (bug 888600)
Backed out changeset 79ef59047e63 (bug 888600)
Backed out changeset 30d568d628dd (bug 888600)
Backed out changeset c7bd4c6c9741 (bug 888600)

--HG--
extra : histedit_source : 791b22f6770f4fead2f909478a93d65d85829fe0%2Cbb387309e90f53e1dde45dcf8cf4ebedcc6e5c5e
2018-03-01 11:51:09 +02:00

554 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/. */
"use strict";
/**
* This file exports XPCOM components for C++ and chrome JavaScript callers to
* interact with the Push service.
*/
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
// The default Push service implementation.
XPCOMUtils.defineLazyGetter(this, "PushService", function() {
const {PushService} = ChromeUtils.import("resource://gre/modules/PushService.jsm",
{});
PushService.init();
return PushService;
});
// 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.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,
Ci.nsIPushErrorReporter,
]),
pushTopic: OBSERVER_TOPIC_PUSH,
subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
_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;
}
if (topic === "android-push-service") {
// Load PushService immediately.
this._handleReady();
return;
}
},
_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.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");
Object.assign(PushServiceParent.prototype, {
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent),
_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, 0, null, callback);
},
subscribeWithKey(scope, principal, keyLen, key, callback) {
this._handleRequest("Push:Register", principal, {
scope: scope,
appServerKey: key,
}).then(result => {
this._deliverSubscription(callback, result);
}, error => {
this._deliverSubscriptionError(callback, error);
}).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 => {
this._deliverSubscriptionError(callback, error);
}).catch(Cu.reportError);
},
clearForDomain(domain, callback) {
return this._handleRequest("Push:Clear", null, {
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);
},
// nsIPushErrorReporter methods
reportDeliveryError(messageId, reason) {
this.service.reportDeliveryError(messageId, reason);
},
receiveMessage(message) {
if (!this._isValidMessage(message)) {
return;
}
let {name, principal, 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;
}
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,
result: error.result,
});
}).catch(Cu.reportError);
},
_handleReady() {
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;
},
_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 || PushService;
},
set(impl) {
this._service = impl;
},
});
/**
* 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) {
this.subscribeWithKey(scope, principal, 0, null, callback);
},
subscribeWithKey(scope, principal, keyLen, key, callback) {
let requestId = this._addRequest(callback);
this._mm.sendAsyncMessage("Push:Register", {
scope: scope,
appServerKey: key,
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);
},
// nsIPushErrorReporter methods
reportDeliveryError(messageId, reason) {
this._mm.sendAsyncMessage("Push:ReportError", {
messageId: messageId,
reason: 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: 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 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, outKeyLen) {
switch (name) {
case "p256dh":
return this._getRawKey(this._props.p256dhKey, outKeyLen);
case "auth":
return this._getRawKey(this._props.authenticationSecret, outKeyLen);
case "appServer":
return this._getRawKey(this._props.appServerKey, outKeyLen);
}
return null;
},
_getRawKey(key, outKeyLen) {
if (!key) {
return null;
}
let rawKey = new Uint8Array(key);
if (outKeyLen) {
outKeyLen.value = rawKey.length;
}
return rawKey;
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
// Export the correct implementation depending on whether we're running in
// the parent or content process.
isParent ? PushServiceParent : PushServiceContent,
]);