diff --git a/dom/apps/src/InterAppComm.manifest b/dom/apps/src/InterAppComm.manifest index 9cacc861b891..aff530ead46d 100644 --- a/dom/apps/src/InterAppComm.manifest +++ b/dom/apps/src/InterAppComm.manifest @@ -13,3 +13,6 @@ category profile-after-change InterAppCommService @mozilla.org/inter-app-communi component {d7c7a466-f91d-11e2-812a-6fab12ece58e} InterAppConnection.js contract @mozilla.org/dom/system-messages/wrapper/connection;1 {d7c7a466-f91d-11e2-812a-6fab12ece58e} + +component {33b4dff4-edf8-11e2-ae9c-77f99f99c3ad} InterAppMessagePort.js +contract @mozilla.org/dom/inter-app-message-event;1 {33b4dff4-edf8-11e2-ae9c-77f99f99c3ad} diff --git a/dom/apps/src/InterAppCommService.js b/dom/apps/src/InterAppCommService.js index 59e7a4e498b0..445511f54527 100644 --- a/dom/apps/src/InterAppCommService.js +++ b/dom/apps/src/InterAppCommService.js @@ -32,7 +32,11 @@ XPCOMUtils.defineLazyServiceGetter(this, "messenger", const kMessages =["Webapps:Connect", "Webapps:GetConnections", - "InterAppConnection:Cancel"]; + "InterAppConnection:Cancel", + "InterAppMessagePort:PostMessage", + "InterAppMessagePort:Register", + "InterAppMessagePort:Unregister", + "child-process-shutdown"]; function InterAppCommService() { Services.obs.addObserver(this, "xpcom-shutdown", false); @@ -167,6 +171,44 @@ function InterAppCommService() { // |requestID| is the ID specifying the promise resolver to return, // |target| is the target of the process requesting the connection. this._promptUICallers = {}; + + // This matrix is used for saving the pair of message ports, which is indexed + // by a random UUID, so that each port can know whom it should talk to. + // An example of the object literal is shown as below: + // + // { + // "UUID1": { + // keyword: "keyword1", + // publisher: { + // manifestURL: "app://pubApp1.gaiamobile.org/manifest.webapp", + // target: pubAppTarget1, + // pageURL: "app://pubApp1.gaiamobile.org/caller.html", + // messageQueue: [...] + // }, + // subscriber: { + // manifestURL: "app://subApp1.gaiamobile.org/manifest.webapp", + // target: subAppTarget1, + // pageURL: "app://pubApp1.gaiamobile.org/handler.html", + // messageQueue: [...] + // } + // }, + // "UUID2": { + // keyword: "keyword2", + // publisher: { + // manifestURL: "app://pubApp2.gaiamobile.org/manifest.webapp", + // target: pubAppTarget2, + // pageURL: "app://pubApp2.gaiamobile.org/caller.html", + // messageQueue: [...] + // }, + // subscriber: { + // manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp", + // target: subAppTarget2, + // pageURL: "app://pubApp2.gaiamobile.org/handler.html", + // messageQueue: [...] + // } + // } + // } + this._messagePortPairs = {}; } InterAppCommService.prototype = { @@ -306,9 +348,11 @@ InterAppCommService.prototype = { return true; }, - _dispatchMessagePorts: function(aKeyword, aAllowedSubAppManifestURLs, + _dispatchMessagePorts: function(aKeyword, aPubAppManifestURL, + aAllowedSubAppManifestURLs, aTarget, aOuterWindowID, aRequestID) { debug("_dispatchMessagePorts: aKeyword: " + aKeyword + + " aPubAppManifestURL: " + aPubAppManifestURL + " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs); if (aAllowedSubAppManifestURLs.length == 0) { @@ -335,7 +379,20 @@ InterAppCommService.prototype = { return; } + // The message port ID is aimed for identifying the coupling targets + // to deliver messages with each other. This ID is centrally generated + // by the parent and dispatched to both the sender and receiver ends + // for creating their own message ports respectively. let messagePortID = UUIDGenerator.generateUUID().toString(); + this._messagePortPairs[messagePortID] = { + keyword: aKeyword, + publisher: { + manifestURL: aPubAppManifestURL + }, + subscriber: { + manifestURL: aAllowedSubAppManifestURL + } + }; // Fire system message to deliver the message port to the subscriber. messenger.sendMessage("connection", @@ -345,7 +402,7 @@ InterAppCommService.prototype = { Services.io.newURI(subscribedInfo.manifestURL, null, null)); messagePortIDs.push(messagePortID); - }); + }, this); if (messagePortIDs.length == 0) { debug("No apps are subscribed to connect. Returning."); @@ -373,7 +430,8 @@ InterAppCommService.prototype = { let subAppManifestURLs = this._registeredConnections[keyword]; if (!subAppManifestURLs) { debug("No apps are subscribed for this connection. Returning.") - this._dispatchMessagePorts(keyword, [], aTarget, outerWindowID, requestID); + this._dispatchMessagePorts(keyword, pubAppManifestURL, [], + aTarget, outerWindowID, requestID); return; } @@ -417,7 +475,8 @@ InterAppCommService.prototype = { if (appsToSelect.length == 0) { debug("No additional apps need to be selected for this connection. " + "Just dispatch message ports for the existing connections."); - this._dispatchMessagePorts(keyword, allowedSubAppManifestURLs, + this._dispatchMessagePorts(keyword, pubAppManifestURL, + allowedSubAppManifestURLs, aTarget, outerWindowID, requestID); return; } @@ -514,6 +573,143 @@ InterAppCommService.prototype = { delete this._allowedConnections[keyword]; } } + + debug("Unregistering message ports based on this connection."); + let messagePortIDs = []; + for (let messagePortID in this._messagePortPairs) { + let pair = this._messagePortPairs[messagePortID]; + if (pair.keyword == keyword && + pair.publisher.manifestURL == pubAppManifestURL && + pair.subscriber.manifestURL == subAppManifestURL) { + messagePortIDs.push(messagePortID); + } + } + messagePortIDs.forEach(function(aMessagePortID) { + delete this._messagePortPairs[aMessagePortID]; + }, this); + }, + + _identifyMessagePort: function(aMessagePortID, aManifestURL) { + let pair = this._messagePortPairs[aMessagePortID]; + if (!pair) { + debug("Error! The message port ID is invalid: " + aMessagePortID + + ", which should have been generated by parent."); + return null; + } + + // Check it the message port is for publisher. + if (pair.publisher.manifestURL == aManifestURL) { + return { pair: pair, isPublisher: true }; + } + + // Check it the message port is for subscriber. + if (pair.subscriber.manifestURL == aManifestURL) { + return { pair: pair, isPublisher: false }; + } + + debug("Error! The manifest URL is invalid: " + aManifestURL + + ", which might be a hacked app."); + return null; + }, + + _registerMessagePort: function(aMessage, aTarget) { + let messagePortID = aMessage.messagePortID; + let manifestURL = aMessage.manifestURL; + let pageURL = aMessage.pageURL; + + let identity = this._identifyMessagePort(messagePortID, manifestURL); + if (!identity) { + debug("Cannot identify the message port. Failed to register."); + return; + } + + debug("Registering message port for " + manifestURL); + let pair = identity.pair; + let isPublisher = identity.isPublisher; + + let sender = isPublisher ? pair.publisher : pair.subscriber; + sender.target = aTarget; + sender.pageURL = pageURL; + sender.messageQueue = []; + + // Check if the other port has queued messages. Deliver them if needed. + debug("Checking if the other port used to send messages but queued."); + let receiver = isPublisher ? pair.subscriber : pair.publisher; + if (receiver.messageQueue) { + while (receiver.messageQueue.length) { + let message = receiver.messageQueue.shift(); + debug("Delivering message: " + JSON.stringify(message)); + sender.target.sendAsyncMessage("InterAppMessagePort:OnMessage", + { message: message, + manifestURL: sender.manifestURL, + pageURL: sender.pageURL, + messagePortID: messagePortID }); + } + } + }, + + _unregisterMessagePort: function(aMessage) { + let messagePortID = aMessage.messagePortID; + let manifestURL = aMessage.manifestURL; + + let identity = this._identifyMessagePort(messagePortID, manifestURL); + if (!identity) { + debug("Cannot identify the message port. Failed to unregister."); + return; + } + + debug("Unregistering message port for " + manifestURL); + delete this._messagePortPairs[messagePortID]; + }, + + _removeTarget: function(aTarget) { + if (!aTarget) { + debug("Error! aTarget cannot be null/undefined in any way."); + return + } + + debug("Unregistering message ports based on this target."); + let messagePortIDs = []; + for (let messagePortID in this._messagePortPairs) { + let pair = this._messagePortPairs[messagePortID]; + if (pair.publisher.target === aTarget || + pair.subscriber.target === aTarget) { + messagePortIDs.push(messagePortID); + } + } + messagePortIDs.forEach(function(aMessagePortID) { + delete this._messagePortPairs[aMessagePortID]; + }, this); + }, + + _postMessage: function(aMessage) { + let messagePortID = aMessage.messagePortID; + let manifestURL = aMessage.manifestURL; + let message = aMessage.message; + + let identity = this._identifyMessagePort(messagePortID, manifestURL); + if (!identity) { + debug("Cannot identify the message port. Failed to post."); + return; + } + + let pair = identity.pair; + let isPublisher = identity.isPublisher; + + let receiver = isPublisher ? pair.subscriber : pair.publisher; + if (!receiver.target) { + debug("The receiver's target is not ready yet. Queuing the message."); + let sender = isPublisher ? pair.publisher : pair.subscriber; + sender.messageQueue.push(message); + return; + } + + debug("Delivering message: " + JSON.stringify(message)); + receiver.target.sendAsyncMessage("InterAppMessagePort:OnMessage", + { manifestURL: receiver.manifestURL, + pageURL: receiver.pageURL, + messagePortID: messagePortID, + message: message }); }, _handleSelectcedApps: function(aData) { @@ -536,7 +732,8 @@ InterAppCommService.prototype = { if (selectedApps.length == 0) { debug("No apps are selected to connect.") - this._dispatchMessagePorts(keyword, [], target, outerWindowID, requestID); + this._dispatchMessagePorts(keyword, manifestURL, [], + target, outerWindowID, requestID); return; } @@ -560,7 +757,7 @@ InterAppCommService.prototype = { // Finally, dispatch the message ports for the allowed connections, // including the old connections and the newly selected connection. - this._dispatchMessagePorts(keyword, allowedSubAppManifestURLs, + this._dispatchMessagePorts(keyword, manifestURL, allowedSubAppManifestURLs, target, outerWindowID, requestID); }, @@ -571,7 +768,8 @@ InterAppCommService.prototype = { // To prevent the hacked child process from sending commands to parent // to do illegal connections, we need to check its manifest URL. - if (kMessages.indexOf(aMessage.name) != -1) { + if (aMessage.name !== "child-process-shutdown" && + kMessages.indexOf(aMessage.name) != -1) { if (!target.assertContainApp(message.manifestURL)) { debug("Got message from a child process carrying illegal manifest URL."); return null; @@ -588,6 +786,18 @@ InterAppCommService.prototype = { case "InterAppConnection:Cancel": this._cancelConnection(message); break; + case "InterAppMessagePort:PostMessage": + this._postMessage(message); + break; + case "InterAppMessagePort:Register": + this._registerMessagePort(message, target); + break; + case "InterAppMessagePort:Unregister": + this._unregisterMessagePort(message); + break; + case "child-process-shutdown": + this._removeTarget(target); + break; } }, diff --git a/dom/apps/src/InterAppMessagePort.js b/dom/apps/src/InterAppMessagePort.js index 5321c511e0ed..735862a41deb 100644 --- a/dom/apps/src/InterAppMessagePort.js +++ b/dom/apps/src/InterAppMessagePort.js @@ -2,57 +2,242 @@ * 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/. */ +// TODO Bug 907060 Per off-line discussion, after the MessagePort is done +// at Bug 643325, we will start to refactorize the common logic of both +// Inter-App Communication and Shared Worker. For now, we hope to design an +// MozInterAppMessagePort to meet the timeline, which still follows exactly +// the same interface and semantic as the MessagePort is. In the future, +// we can then align it back to MessagePort with backward compatibility. + "use strict"; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); +Cu.import("resource://gre/modules/ObjectWrapper.jsm"); function debug(aMsg) { // dump("-- InterAppMessagePort: " + Date.now() + ": " + aMsg + "\n"); } +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +const kMessages = ["InterAppMessagePort:OnMessage"]; + + +function InterAppMessageEvent() { + this.type = this.data = null; +}; + +InterAppMessageEvent.prototype = { + classDescription: "MozInterAppMessageEvent", + + classID: Components.ID("{33b4dff4-edf8-11e2-ae9c-77f99f99c3ad}"), + + contractID: "@mozilla.org/dom/inter-app-message-event;1", + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + __init: function(aType, aDict) { + this.type = aType; + this.__DOM_IMPL__.initEvent(aType, aDict.bubbles || false, + aDict.cancelable || false); + this.data = aDict.data; + } +}; + + function InterAppMessagePort() { debug("InterAppMessagePort()"); }; InterAppMessagePort.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + classDescription: "MozInterAppMessagePort", classID: Components.ID("{c66e0f8c-e3cb-11e2-9e85-43ef6244b884}"), contractID: "@mozilla.org/dom/inter-app-message-port;1", - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsISupportsWeakReference]), + + // Ci.nsIDOMGlobalPropertyInitializer implementation. + init: function(aWindow) { + debug("Calling init()."); + + this.initDOMRequestHelper(aWindow, kMessages); + + let principal = aWindow.document.nodePrincipal; + this._manifestURL = appsService.getManifestURLByLocalId(principal.appId); + this._pageURL = principal.URI.spec; + + this._started = false; + this._closed = false; + this._messageQueue = []; + }, // WebIDL implementation for constructor. __init: function(aKeyword, aMessagePortID, aIsPublisher) { debug("Calling __init(): aKeyword: " + aKeyword + " aMessagePortID: " + aMessagePortID + " aIsPublisher: " + aIsPublisher); + + this._keyword = aKeyword; + this._messagePortID = aMessagePortID; + this._isPublisher = aIsPublisher; + + cpmm.sendAsyncMessage("InterAppMessagePort:Register", + { messagePortID: this._messagePortID, + manifestURL: this._manifestURL, + pageURL: this._pageURL }); + }, + + // DOMRequestIpcHelper implementation. + uninit: function() { + debug("Calling uninit()."); + + // When the message port is uninitialized, we need to disentangle the + // coupling ports, as if the close() method had been called. + if (this._closed) { + debug("close() has been called. Don't need to close again."); + return; + } + + this.close(); }, postMessage: function(aMessage) { - // TODO + debug("Calling postMessage()."); + + if (this._closed) { + debug("close() has been called. Cannot post message."); + return; + } + + cpmm.sendAsyncMessage("InterAppMessagePort:PostMessage", + { messagePortID: this._messagePortID, + manifestURL: this._manifestURL, + message: aMessage }); }, start: function() { - // TODO + // Begin dispatching messages received on the port. + debug("Calling start()."); + + if (this._closed) { + debug("close() has been called. Cannot call start()."); + return; + } + + if (this._started) { + debug("start() has been called. Don't need to start again."); + return; + } + + // When a port's port message queue is enabled, the event loop must use it + // as one of its task sources. + this._started = true; + while (this._messageQueue.length) { + let message = this._messageQueue.shift(); + this._dispatchMessage(message); + } }, close: function() { - // TODO + // Disconnecting the port, so that it is no longer active. + debug("Calling close()."); + + if (this._closed) { + debug("close() has been called. Don't need to close again."); + return; + } + + this._closed = true; + this._messageQueue.length = 0; + + // When this method called on a local port that is entangled with another + // port, must cause the user agent to disentangle the coupling ports. + cpmm.sendAsyncMessage("InterAppMessagePort:Unregister", + { messagePortID: this._messagePortID, + manifestURL: this._manifestURL }); }, get onmessage() { + debug("Getting onmessage handler."); + return this.__DOM_IMPL__.getEventHandler("onmessage"); }, set onmessage(aHandler) { + debug("Setting onmessage handler."); + this.__DOM_IMPL__.setEventHandler("onmessage", aHandler); + + // The first time a MessagePort object's onmessage IDL attribute is set, + // the port's message queue must be enabled, as if the start() method had + // been called. + if (this._started) { + debug("start() has been called. Don't need to start again."); + return; + } + + this.start(); + }, + + _dispatchMessage: function _dispatchMessage(aMessage) { + let wrappedMessage = ObjectWrapper.wrap(aMessage, this._window); + debug("_dispatchMessage: wrappedMessage: " + JSON.stringify(wrappedMessage)); + + let event = new this._window + .MozInterAppMessageEvent("message", + { data: wrappedMessage }); + this.__DOM_IMPL__.dispatchEvent(event); + }, + + receiveMessage: function(aMessage) { + debug("receiveMessage: name: " + aMessage.name); + + let message = aMessage.json; + if (message.manifestURL != this._manifestURL || + message.pageURL != this._pageURL || + message.messagePortID != this._messagePortID) { + debug("The message doesn't belong to this page. Returning."); + return; + } + + switch (aMessage.name) { + case "InterAppMessagePort:OnMessage": + if (this._closed) { + debug("close() has been called. Drop the message."); + return; + } + + if (!this._started) { + debug("Not yet called start(). Queue up the message."); + this._messageQueue.push(message.message); + return; + } + + this._dispatchMessage(message.message); + break; + + default: + debug("Error! Shouldn't fall into this case."); + break; + } } }; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppMessagePort]); +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppMessagePort, + InterAppMessageEvent]); diff --git a/dom/webidl/InterAppMessageEvent.webidl b/dom/webidl/InterAppMessageEvent.webidl new file mode 100644 index 000000000000..33734a5fa036 --- /dev/null +++ b/dom/webidl/InterAppMessageEvent.webidl @@ -0,0 +1,16 @@ +/* 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/. */ + +dictionary MozInterAppMessageEventInit : EventInit { + any data; +}; + +[HeaderFile="mozilla/dom/InterAppComm.h", + Func="mozilla::dom::InterAppComm::EnabledForScope", + Constructor(DOMString type, + optional MozInterAppMessageEventInit eventInitDict), + JSImplementation="@mozilla.org/dom/inter-app-message-event;1"] +interface MozInterAppMessageEvent : Event { + readonly attribute any data; +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 07d4ca51a4e2..d0aa0c1d1b7c 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -188,6 +188,7 @@ WEBIDL_FILES = [ 'InspectorUtils.webidl', 'InterAppConnection.webidl', 'InterAppConnectionRequest.webidl', + 'InterAppMessageEvent.webidl', 'InterAppMessagePort.webidl', 'KeyboardEvent.webidl', 'KeyEvent.webidl',