diff -r 000000000000 -r 6474c204b198 dom/apps/src/InterAppCommService.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/apps/src/InterAppCommService.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,889 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +this.EXPORTED_SYMBOLS = ["InterAppCommService"]; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/AppsUtils.jsm"); + +const DEBUG = false; +function debug(aMsg) { + dump("-- InterAppCommService: " + Date.now() + ": " + aMsg + "\n"); +} + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); + +XPCOMUtils.defineLazyServiceGetter(this, "UUIDGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +XPCOMUtils.defineLazyServiceGetter(this, "messenger", + "@mozilla.org/system-message-internal;1", + "nsISystemMessagesInternal"); + +const kMessages =["Webapps:Connect", + "Webapps:GetConnections", + "InterAppConnection:Cancel", + "InterAppMessagePort:PostMessage", + "InterAppMessagePort:Register", + "InterAppMessagePort:Unregister", + "child-process-shutdown"]; + +/** + * This module contains helpers for Inter-App Communication API [1] related + * purposes, which plays the role of the central service receiving messages + * from and interacting with the content processes. + * + * [1] https://wiki.mozilla.org/WebAPI/Inter_App_Communication_Alt_proposal + */ + +this.InterAppCommService = { + init: function() { + Services.obs.addObserver(this, "xpcom-shutdown", false); + Services.obs.addObserver(this, "inter-app-comm-select-app-result", false); + + kMessages.forEach(function(aMsg) { + ppmm.addMessageListener(aMsg, this); + }, this); + + // This matrix is used for saving the inter-app connection info registered in + // the app manifest. The object literal is defined as below: + // + // { + // "keyword1": { + // "subAppManifestURL1": { + // /* subscribed info */ + // }, + // "subAppManifestURL2": { + // /* subscribed info */ + // }, + // ... + // }, + // "keyword2": { + // "subAppManifestURL3": { + // /* subscribed info */ + // }, + // ... + // }, + // ... + // } + // + // For example: + // + // { + // "foo": { + // "app://subApp1.gaiamobile.org/manifest.webapp": { + // pageURL: "app://subApp1.gaiamobile.org/handler.html", + // description: "blah blah", + // rules: { ... } + // }, + // "app://subApp2.gaiamobile.org/manifest.webapp": { + // pageURL: "app://subApp2.gaiamobile.org/handler.html", + // description: "blah blah", + // rules: { ... } + // } + // }, + // "bar": { + // "app://subApp3.gaiamobile.org/manifest.webapp": { + // pageURL: "app://subApp3.gaiamobile.org/handler.html", + // description: "blah blah", + // rules: { ... } + // } + // } + // } + // + // TODO Bug 908999 - Update registered connections when app gets uninstalled. + this._registeredConnections = {}; + + // This matrix is used for saving the permitted connections, which allows + // the messaging between publishers and subscribers. The object literal is + // defined as below: + // + // { + // "keyword1": { + // "pubAppManifestURL1": [ + // "subAppManifestURL1", + // "subAppManifestURL2", + // ... + // ], + // "pubAppManifestURL2": [ + // "subAppManifestURL3", + // "subAppManifestURL4", + // ... + // ], + // ... + // }, + // "keyword2": { + // "pubAppManifestURL3": [ + // "subAppManifestURL5", + // ... + // ], + // ... + // }, + // ... + // } + // + // For example: + // + // { + // "foo": { + // "app://pubApp1.gaiamobile.org/manifest.webapp": [ + // "app://subApp1.gaiamobile.org/manifest.webapp", + // "app://subApp2.gaiamobile.org/manifest.webapp" + // ], + // "app://pubApp2.gaiamobile.org/manifest.webapp": [ + // "app://subApp3.gaiamobile.org/manifest.webapp", + // "app://subApp4.gaiamobile.org/manifest.webapp" + // ] + // }, + // "bar": { + // "app://pubApp3.gaiamobile.org/manifest.webapp": [ + // "app://subApp5.gaiamobile.org/manifest.webapp", + // ] + // } + // } + // + // TODO Bug 908999 - Update allowed connections when app gets uninstalled. + this._allowedConnections = {}; + + // This matrix is used for saving the caller info from the content process, + // which is indexed by a random UUID, to know where to return the promise + // resolvser's callback when the prompt UI for allowing connections returns. + // An example of the object literal is shown as below: + // + // { + // "fooID": { + // outerWindowID: 12, + // requestID: 34, + // target: pubAppTarget1 + // }, + // "barID": { + // outerWindowID: 56, + // requestID: 78, + // target: pubAppTarget2 + // } + // } + // + // where |outerWindowID| is the ID of the window requesting the connection, + // |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 = {}; + }, + + /** + * Registration of a page that wants to be connected to other apps through + * the Inter-App Communication API. + * + * @param aKeyword The connection's keyword. + * @param aHandlerPageURI The URI of the handler's page. + * @param aManifestURI The webapp's manifest URI. + * @param aDescription The connection's description. + * @param aRules The connection's rules. + */ + registerConnection: function(aKeyword, aHandlerPageURI, aManifestURI, + aDescription, aRules) { + let manifestURL = aManifestURI.spec; + let pageURL = aHandlerPageURI.spec; + + if (DEBUG) { + debug("registerConnection: aKeyword: " + aKeyword + + " manifestURL: " + manifestURL + " pageURL: " + pageURL + + " aDescription: " + aDescription + + " aRules.minimumAccessLevel: " + aRules.minimumAccessLevel + + " aRules.manifestURLs: " + aRules.manifestURLs + + " aRules.installOrigins: " + aRules.installOrigins); + } + + let subAppManifestURLs = this._registeredConnections[aKeyword]; + if (!subAppManifestURLs) { + subAppManifestURLs = this._registeredConnections[aKeyword] = {}; + } + + subAppManifestURLs[manifestURL] = { + pageURL: pageURL, + description: aDescription, + rules: aRules, + manifestURL: manifestURL + }; + }, + + _matchMinimumAccessLevel: function(aRules, aAppStatus) { + if (!aRules || !aRules.minimumAccessLevel) { + if (DEBUG) { + debug("rules.minimumAccessLevel is not available. No need to match."); + } + return true; + } + + let minAccessLevel = aRules.minimumAccessLevel; + switch (minAccessLevel) { + case "web": + if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_INSTALLED || + aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED || + aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) { + return true; + } + break; + case "privileged": + if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED || + aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) { + return true; + } + break; + case "certified": + if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) { + return true; + } + break; + } + + if (DEBUG) { + debug("rules.minimumAccessLevel is not matched!" + + " minAccessLevel: " + minAccessLevel + + " aAppStatus : " + aAppStatus); + } + return false; + }, + + _matchManifestURLs: function(aRules, aManifestURL) { + if (!aRules || !Array.isArray(aRules.manifestURLs)) { + if (DEBUG) { + debug("rules.manifestURLs is not available. No need to match."); + } + return true; + } + + let manifestURLs = aRules.manifestURLs; + if (manifestURLs.indexOf(aManifestURL) != -1) { + return true; + } + + if (DEBUG) { + debug("rules.manifestURLs is not matched!" + + " manifestURLs: " + manifestURLs + + " aManifestURL : " + aManifestURL); + } + return false; + }, + + _matchInstallOrigins: function(aRules, aInstallOrigin) { + if (!aRules || !Array.isArray(aRules.installOrigins)) { + if (DEBUG) { + debug("rules.installOrigins is not available. No need to match."); + } + return true; + } + + let installOrigins = aRules.installOrigins; + if (installOrigins.indexOf(aInstallOrigin) != -1) { + return true; + } + + if (DEBUG) { + debug("rules.installOrigins is not matched!" + + " installOrigins: " + installOrigins + + " installOrigin : " + aInstallOrigin); + } + return false; + }, + + _matchRules: function(aPubAppManifestURL, aPubRules, + aSubAppManifestURL, aSubRules) { + let pubApp = appsService.getAppByManifestURL(aPubAppManifestURL); + let subApp = appsService.getAppByManifestURL(aSubAppManifestURL); + + // TODO Bug 907068 In the initiative step, we only expose this API to + // certified apps to meet the time line. Eventually, we need to make + // it available for the non-certified apps as well. For now, only the + // certified apps can match the rules. + if (pubApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED || + subApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED) { + if (DEBUG) { + debug("Only certified apps are allowed to do connections."); + } + return false; + } + + if (!aPubRules && !aSubRules) { + if (DEBUG) { + debug("No rules for publisher and subscriber. No need to match."); + } + return true; + } + + // Check minimumAccessLevel. + if (!this._matchMinimumAccessLevel(aPubRules, subApp.appStatus) || + !this._matchMinimumAccessLevel(aSubRules, pubApp.appStatus)) { + return false; + } + + // Check manifestURLs. + if (!this._matchManifestURLs(aPubRules, aSubAppManifestURL) || + !this._matchManifestURLs(aSubRules, aPubAppManifestURL)) { + return false; + } + + // Check installOrigins. + if (!this._matchInstallOrigins(aPubRules, subApp.installOrigin) || + !this._matchInstallOrigins(aSubRules, pubApp.installOrigin)) { + return false; + } + + // Check developers. + // TODO Do we really want to check this? This one seems naive. + + if (DEBUG) debug("All rules are matched."); + return true; + }, + + _dispatchMessagePorts: function(aKeyword, aPubAppManifestURL, + aAllowedSubAppManifestURLs, + aTarget, aOuterWindowID, aRequestID) { + if (DEBUG) { + debug("_dispatchMessagePorts: aKeyword: " + aKeyword + + " aPubAppManifestURL: " + aPubAppManifestURL + + " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs); + } + + if (aAllowedSubAppManifestURLs.length == 0) { + if (DEBUG) debug("No apps are allowed to connect. Returning."); + aTarget.sendAsyncMessage("Webapps:Connect:Return:KO", + { oid: aOuterWindowID, requestID: aRequestID }); + return; + } + + let subAppManifestURLs = this._registeredConnections[aKeyword]; + if (!subAppManifestURLs) { + if (DEBUG) debug("No apps are subscribed to connect. Returning."); + aTarget.sendAsyncMessage("Webapps:Connect:Return:KO", + { oid: aOuterWindowID, requestID: aRequestID }); + return; + } + + let messagePortIDs = []; + aAllowedSubAppManifestURLs.forEach(function(aAllowedSubAppManifestURL) { + let subscribedInfo = subAppManifestURLs[aAllowedSubAppManifestURL]; + if (!subscribedInfo) { + if (DEBUG) { + debug("The sunscribed info is not available. Skipping: " + + aAllowedSubAppManifestURL); + } + 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", + { keyword: aKeyword, + messagePortID: messagePortID }, + Services.io.newURI(subscribedInfo.pageURL, null, null), + Services.io.newURI(subscribedInfo.manifestURL, null, null)); + + messagePortIDs.push(messagePortID); + }, this); + + if (messagePortIDs.length == 0) { + if (DEBUG) debug("No apps are subscribed to connect. Returning."); + aTarget.sendAsyncMessage("Webapps:Connect:Return:KO", + { oid: aOuterWindowID, requestID: aRequestID }); + return; + } + + // Return the message port IDs to open the message ports for the publisher. + if (DEBUG) debug("messagePortIDs: " + messagePortIDs); + aTarget.sendAsyncMessage("Webapps:Connect:Return:OK", + { keyword: aKeyword, + messagePortIDs: messagePortIDs, + oid: aOuterWindowID, requestID: aRequestID }); + }, + + _connect: function(aMessage, aTarget) { + let keyword = aMessage.keyword; + let pubRules = aMessage.rules; + let pubAppManifestURL = aMessage.manifestURL; + let outerWindowID = aMessage.outerWindowID; + let requestID = aMessage.requestID; + + let subAppManifestURLs = this._registeredConnections[keyword]; + if (!subAppManifestURLs) { + if (DEBUG) { + debug("No apps are subscribed for this connection. Returning."); + } + this._dispatchMessagePorts(keyword, pubAppManifestURL, [], + aTarget, outerWindowID, requestID); + return; + } + + // Fetch the apps that used to be allowed to connect before, so that + // users don't need to select/allow them again. That is, we only pop up + // the prompt UI for the *new* connections. + let allowedSubAppManifestURLs = []; + let allowedPubAppManifestURLs = this._allowedConnections[keyword]; + if (allowedPubAppManifestURLs && + allowedPubAppManifestURLs[pubAppManifestURL]) { + allowedSubAppManifestURLs = allowedPubAppManifestURLs[pubAppManifestURL]; + } + + // Check rules to see if a subscribed app is allowed to connect. + let appsToSelect = []; + for (let subAppManifestURL in subAppManifestURLs) { + if (allowedSubAppManifestURLs.indexOf(subAppManifestURL) != -1) { + if (DEBUG) { + debug("Don't need to select again. Skipping: " + subAppManifestURL); + } + continue; + } + + // Only rule-matched publishers/subscribers are allowed to connect. + let subscribedInfo = subAppManifestURLs[subAppManifestURL]; + let subRules = subscribedInfo.rules; + + let matched = + this._matchRules(pubAppManifestURL, pubRules, + subAppManifestURL, subRules); + if (!matched) { + if (DEBUG) { + debug("Rules are not matched. Skipping: " + subAppManifestURL); + } + continue; + } + + appsToSelect.push({ + manifestURL: subAppManifestURL, + description: subscribedInfo.description + }); + } + + if (appsToSelect.length == 0) { + if (DEBUG) { + debug("No additional apps need to be selected for this connection. " + + "Just dispatch message ports for the existing connections."); + } + + this._dispatchMessagePorts(keyword, pubAppManifestURL, + allowedSubAppManifestURLs, + aTarget, outerWindowID, requestID); + return; + } + + // Remember the caller info with an UUID so that we can know where to + // return the promise resolver's callback when the prompt UI returns. + let callerID = UUIDGenerator.generateUUID().toString(); + this._promptUICallers[callerID] = { + outerWindowID: outerWindowID, + requestID: requestID, + target: aTarget + }; + + // TODO Bug 897169 Temporarily disable the notification for popping up + // the prompt until the UX/UI for the prompt is confirmed. + // + // TODO Bug 908191 We need to change the way of interaction between API and + // run-time prompt from observer notification to xpcom-interface caller. + // + /* + if (DEBUG) debug("appsToSelect: " + appsToSelect); + Services.obs.notifyObservers(null, "inter-app-comm-select-app", + JSON.stringify({ callerID: callerID, + manifestURL: pubAppManifestURL, + keyword: keyword, + appsToSelect: appsToSelect })); + */ + + // TODO Bug 897169 Simulate the return of the app-selected result by + // the prompt, which always allows the connection. This dummy codes + // will be removed when the UX/UI for the prompt is ready. + if (DEBUG) debug("appsToSelect: " + appsToSelect); + Services.obs.notifyObservers(null, 'inter-app-comm-select-app-result', + JSON.stringify({ callerID: callerID, + manifestURL: pubAppManifestURL, + keyword: keyword, + selectedApps: appsToSelect })); + }, + + _getConnections: function(aMessage, aTarget) { + let outerWindowID = aMessage.outerWindowID; + let requestID = aMessage.requestID; + + let connections = []; + for (let keyword in this._allowedConnections) { + let allowedPubAppManifestURLs = this._allowedConnections[keyword]; + for (let allowedPubAppManifestURL in allowedPubAppManifestURLs) { + let allowedSubAppManifestURLs = + allowedPubAppManifestURLs[allowedPubAppManifestURL]; + allowedSubAppManifestURLs.forEach(function(allowedSubAppManifestURL) { + connections.push({ keyword: keyword, + pubAppManifestURL: allowedPubAppManifestURL, + subAppManifestURL: allowedSubAppManifestURL }); + }); + } + } + + aTarget.sendAsyncMessage("Webapps:GetConnections:Return:OK", + { connections: connections, + oid: outerWindowID, requestID: requestID }); + }, + + _cancelConnection: function(aMessage) { + let keyword = aMessage.keyword; + let pubAppManifestURL = aMessage.pubAppManifestURL; + let subAppManifestURL = aMessage.subAppManifestURL; + + let allowedPubAppManifestURLs = this._allowedConnections[keyword]; + if (!allowedPubAppManifestURLs) { + if (DEBUG) debug("keyword is not found: " + keyword); + return; + } + + let allowedSubAppManifestURLs = + allowedPubAppManifestURLs[pubAppManifestURL]; + if (!allowedSubAppManifestURLs) { + if (DEBUG) debug("publisher is not found: " + pubAppManifestURL); + return; + } + + let index = allowedSubAppManifestURLs.indexOf(subAppManifestURL); + if (index == -1) { + if (DEBUG) debug("subscriber is not found: " + subAppManifestURL); + return; + } + + if (DEBUG) debug("Cancelling the connection."); + allowedSubAppManifestURLs.splice(index, 1); + + // Clean up the parent entries if needed. + if (allowedSubAppManifestURLs.length == 0) { + delete allowedPubAppManifestURLs[pubAppManifestURL]; + if (Object.keys(allowedPubAppManifestURLs).length == 0) { + delete this._allowedConnections[keyword]; + } + } + + if (DEBUG) 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) { + if (DEBUG) { + 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 }; + } + + if (DEBUG) { + 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) { + if (DEBUG) { + debug("Cannot identify the message port. Failed to register."); + } + return; + } + + if (DEBUG) 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. + if (DEBUG) { + 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(); + if (DEBUG) 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) { + if (DEBUG) { + debug("Cannot identify the message port. Failed to unregister."); + } + return; + } + + if (DEBUG) { + debug("Unregistering message port for " + manifestURL); + } + delete this._messagePortPairs[messagePortID]; + }, + + _removeTarget: function(aTarget) { + if (!aTarget) { + if (DEBUG) debug("Error! aTarget cannot be null/undefined in any way."); + return + } + + if (DEBUG) 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) { + if (DEBUG) 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) { + if (DEBUG) { + debug("The receiver's target is not ready yet. Queuing the message."); + } + let sender = isPublisher ? pair.publisher : pair.subscriber; + sender.messageQueue.push(message); + return; + } + + if (DEBUG) debug("Delivering message: " + JSON.stringify(message)); + receiver.target.sendAsyncMessage("InterAppMessagePort:OnMessage", + { manifestURL: receiver.manifestURL, + pageURL: receiver.pageURL, + messagePortID: messagePortID, + message: message }); + }, + + _handleSelectcedApps: function(aData) { + let callerID = aData.callerID; + let caller = this._promptUICallers[callerID]; + if (!caller) { + if (DEBUG) debug("Error! Cannot find the caller."); + return; + } + + delete this._promptUICallers[callerID]; + + let outerWindowID = caller.outerWindowID; + let requestID = caller.requestID; + let target = caller.target; + + let manifestURL = aData.manifestURL; + let keyword = aData.keyword; + let selectedApps = aData.selectedApps; + + if (selectedApps.length == 0) { + if (DEBUG) debug("No apps are selected to connect.") + this._dispatchMessagePorts(keyword, manifestURL, [], + target, outerWindowID, requestID); + return; + } + + // Find the entry of allowed connections to add the selected apps. + let allowedPubAppManifestURLs = this._allowedConnections[keyword]; + if (!allowedPubAppManifestURLs) { + allowedPubAppManifestURLs = this._allowedConnections[keyword] = {}; + } + let allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL]; + if (!allowedSubAppManifestURLs) { + allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL] = []; + } + + // Add the selected app into the existing set of allowed connections. + selectedApps.forEach(function(aSelectedApp) { + let allowedSubAppManifestURL = aSelectedApp.manifestURL; + if (allowedSubAppManifestURLs.indexOf(allowedSubAppManifestURL) == -1) { + allowedSubAppManifestURLs.push(allowedSubAppManifestURL); + } + }); + + // Finally, dispatch the message ports for the allowed connections, + // including the old connections and the newly selected connection. + this._dispatchMessagePorts(keyword, manifestURL, allowedSubAppManifestURLs, + target, outerWindowID, requestID); + }, + + receiveMessage: function(aMessage) { + if (DEBUG) debug("receiveMessage: name: " + aMessage.name); + let message = aMessage.json; + let target = aMessage.target; + + // To prevent the hacked child process from sending commands to parent + // to do illegal connections, we need to check its manifest URL. + if (aMessage.name !== "child-process-shutdown" && + // TODO: fix bug 988142 to re-enable "InterAppMessagePort:Unregister". + aMessage.name !== "InterAppMessagePort:Unregister" && + kMessages.indexOf(aMessage.name) != -1) { + if (!target.assertContainApp(message.manifestURL)) { + if (DEBUG) { + debug("Got message from a process carrying illegal manifest URL."); + } + return null; + } + } + + switch (aMessage.name) { + case "Webapps:Connect": + this._connect(message, target); + break; + case "Webapps:GetConnections": + this._getConnections(message, target); + break; + 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; + } + }, + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "xpcom-shutdown": + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.obs.removeObserver(this, "inter-app-comm-select-app-result"); + kMessages.forEach(function(aMsg) { + ppmm.removeMessageListener(aMsg, this); + }, this); + ppmm = null; + break; + case "inter-app-comm-select-app-result": + if (DEBUG) debug("inter-app-comm-select-app-result: " + aData); + this._handleSelectcedApps(JSON.parse(aData)); + break; + } + } +}; + +InterAppCommService.init();