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