From 7ee0993f67d5f70e02dbc7012f90708db79e6bc2 Mon Sep 17 00:00:00 2001 From: Gene Lian Date: Fri, 16 Aug 2013 17:49:15 +0800 Subject: [PATCH] Bug 876397 - Inter-App Communication API (part 3, connect()). r=nsm --- b2g/chrome/content/shell.js | 20 +- dom/apps/src/InterAppCommService.js | 437 ++++++++++++++++++++++++++ dom/apps/src/InterAppMessagePort.js | 7 + dom/apps/src/Webapps.js | 36 ++- dom/webidl/InterAppMessagePort.webidl | 1 + 5 files changed, 496 insertions(+), 5 deletions(-) diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index 6a393a1c596..d89726432ea 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -631,9 +631,18 @@ Services.obs.addObserver(function onSystemMessageOpenApp(subject, topic, data) { shell.openAppForSystemMessage(msg); }, 'system-messages-open-app', false); -Services.obs.addObserver(function(aSubject, aTopic, aData) { +Services.obs.addObserver(function onInterAppCommConnect(subject, topic, data) { + data = JSON.parse(data); + shell.sendChromeEvent({ type: "inter-app-comm-permission", + chromeEventID: data.callerID, + manifestURL: data.manifestURL, + keyword: data.keyword, + peers: data.appsToSelect }); +}, 'inter-app-comm-select-app', false); + +Services.obs.addObserver(function onFullscreenOriginChange(subject, topic, data) { shell.sendChromeEvent({ type: "fullscreenoriginchange", - fullscreenorigin: aData }); + fullscreenorigin: data }); }, "fullscreen-origin-change", false); Services.obs.addObserver(function onWebappsStart(subject, topic, data) { @@ -700,6 +709,13 @@ var CustomEventManager = { case 'captive-portal-login-cancel': CaptivePortalLoginHelper.handleEvent(detail); break; + case 'inter-app-comm-permission': + Services.obs.notifyObservers(null, 'inter-app-comm-select-app-result', + JSON.stringify({ callerID: detail.chromeEventID, + keyword: detail.keyword, + manifestURL: detail.manifestURL, + selectedApps: detail.peers })); + break; } } } diff --git a/dom/apps/src/InterAppCommService.js b/dom/apps/src/InterAppCommService.js index f8125eb0fcb..341ab2611b5 100644 --- a/dom/apps/src/InterAppCommService.js +++ b/dom/apps/src/InterAppCommService.js @@ -8,13 +8,37 @@ 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/AppsUtils.jsm"); 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"]; + function InterAppCommService() { 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: @@ -67,6 +91,80 @@ function InterAppCommService() { // // 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 = {}; } InterAppCommService.prototype = { @@ -96,10 +194,349 @@ InterAppCommService.prototype = { }; }, + _matchMinimumAccessLevel: function(aRules, aAppStatus) { + if (!aRules || !aRules.minimumAccessLevel) { + 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; + } + + debug("rules.minimumAccessLevel is not matched! " + + "minAccessLevel: " + minAccessLevel + " aAppStatus : " + aAppStatus); + return false; + }, + + _matchManifestURLs: function(aRules, aManifestURL) { + if (!aRules || !Array.isArray(aRules.manifestURLs)) { + debug("rules.manifestURLs is not available. No need to match."); + return true; + } + + let manifestURLs = aRules.manifestURLs; + if (manifestURLs.indexOf(aManifestURL) != -1) { + return true; + } + + debug("rules.manifestURLs is not matched! " + + "manifestURLs: " + manifestURLs + " aManifestURL : " + aManifestURL); + return false; + }, + + _matchInstallOrigins: function(aRules, aManifestURL) { + if (!aRules || !Array.isArray(aRules.installOrigins)) { + debug("rules.installOrigins is not available. No need to match."); + return true; + } + + let installOrigin = + appsService.getAppByManifestURL(aManifestURL).installOrigin; + + let installOrigins = aRules.installOrigins; + if (installOrigins.indexOf(installOrigin) != -1) { + return true; + } + + debug("rules.installOrigins is not matched! aManifestURL: " + aManifestURL + + " installOrigins: " + installOrigins + " installOrigin : " + installOrigin); + return false; + }, + + _matchRules: function(aPubAppManifestURL, aPubAppStatus, aPubRules, + aSubAppManifestURL, aSubAppStatus, aSubRules) { + // 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 (aPubAppStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED || + aSubAppStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED) { + debug("Only certified apps are allowed to do connections."); + return false; + } + + if (!aPubRules && !aSubRules) { + debug("Rules of publisher and subscriber are absent. No need to match."); + return true; + } + + // Check minimumAccessLevel. + if (!this._matchMinimumAccessLevel(aPubRules, aSubAppStatus) || + !this._matchMinimumAccessLevel(aSubRules, aPubAppStatus)) { + return false; + } + + // Check manifestURLs. + if (!this._matchManifestURLs(aPubRules, aSubAppManifestURL) || + !this._matchManifestURLs(aSubRules, aPubAppManifestURL)) { + return false; + } + + // Check installOrigins. + if (!this._matchInstallOrigins(aPubRules, aSubAppManifestURL) || + !this._matchInstallOrigins(aSubRules, aPubAppManifestURL)) { + return false; + } + + // Check developers. + // TODO Do we really want to check this? This one seems naive. + + debug("All rules are matched."); + return true; + }, + + _dispatchMessagePorts: function(aKeyword, aAllowedSubAppManifestURLs, + aTarget, aOuterWindowID, aRequestID) { + debug("_dispatchMessagePorts: aKeyword: " + aKeyword + + " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs); + + if (aAllowedSubAppManifestURLs.length == 0) { + 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) { + 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) { + debug("The sunscribed info is not available. Skipping: " + + aAllowedSubAppManifestURL); + return; + } + + let messagePortID = UUIDGenerator.generateUUID().toString(); + + // 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); + }); + + if (messagePortIDs.length == 0) { + 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. + 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 pubAppStatus = aMessage.appStatus; + + let subAppManifestURLs = this._registeredConnections[keyword]; + if (!subAppManifestURLs) { + debug("No apps are subscribed for this connection. Returning.") + this._dispatchMessagePorts(keyword, [], 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) { + debug("Don't need to select again. Skipping: " + subAppManifestURL); + continue; + } + + // Only rule-matched publishers/subscribers are allowed to connect. + let subscribedInfo = subAppManifestURLs[subAppManifestURL]; + let subAppStatus = subscribedInfo.appStatus; + let subRules = subscribedInfo.rules; + + let matched = + this._matchRules(pubAppManifestURL, pubAppStatus, pubRules, + subAppManifestURL, subAppStatus, subRules); + if (!matched) { + debug("Rules are not matched. Skipping: " + subAppManifestURL); + continue; + } + + appsToSelect.push({ + manifestURL: subAppManifestURL, + description: subscribedInfo.description + }); + } + + 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, + 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. + // + /* + 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. + debug("appsToSelect: " + appsToSelect); + Services.obs.notifyObservers(null, 'inter-app-comm-select-app-result', + JSON.stringify({ callerID: callerID, + manifestURL: pubAppManifestURL, + keyword: keyword, + selectedApps: appsToSelect })); + }, + + _handleSelectcedApps: function(aData) { + let callerID = aData.callerID; + let caller = this._promptUICallers[callerID]; + if (!caller) { + 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) { + debug("No apps are selected to connect.") + this._dispatchMessagePorts(keyword, [], 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, allowedSubAppManifestURLs, + target, outerWindowID, requestID); + }, + + receiveMessage: function(aMessage) { + 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 (kMessages.indexOf(aMessage.name) != -1) { + if (!target.assertContainApp(message.manifestURL)) { + debug("Got message from a child process carrying illegal manifest URL."); + return null; + } + } + + switch (aMessage.name) { + case "Webapps:Connect": + this._connect(message, 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": + debug("inter-app-comm-select-app-result: " + aData); + this._handleSelectcedApps(JSON.parse(aData)); break; } }, diff --git a/dom/apps/src/InterAppMessagePort.js b/dom/apps/src/InterAppMessagePort.js index fd2e320f1bf..5321c511e0e 100644 --- a/dom/apps/src/InterAppMessagePort.js +++ b/dom/apps/src/InterAppMessagePort.js @@ -26,6 +26,13 @@ InterAppMessagePort.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + // WebIDL implementation for constructor. + __init: function(aKeyword, aMessagePortID, aIsPublisher) { + debug("Calling __init(): aKeyword: " + aKeyword + + " aMessagePortID: " + aMessagePortID + + " aIsPublisher: " + aIsPublisher); + }, + postMessage: function(aMessage) { // TODO }, diff --git a/dom/apps/src/Webapps.js b/dom/apps/src/Webapps.js index 9b62ada7fce..d7f7bf0d8c4 100644 --- a/dom/apps/src/Webapps.js +++ b/dom/apps/src/Webapps.js @@ -309,6 +309,8 @@ WebappsApplication.prototype = { init: function(aWindow, aApp) { this._window = aWindow; + let principal = this._window.document.nodePrincipal; + this._appStatus = principal.appStatus; this.origin = aApp.origin; this._manifest = aApp.manifest; this._updateManifest = aApp.updateManifest; @@ -342,7 +344,9 @@ WebappsApplication.prototype = { "Webapps:Launch:Return:OK", "Webapps:Launch:Return:KO", "Webapps:PackageEvent", - "Webapps:ClearBrowserData:Return"]); + "Webapps:ClearBrowserData:Return", + "Webapps:Connect:Return:OK", + "Webapps:Connect:Return:KO"]); cpmm.sendAsyncMessage("Webapps:RegisterForMessages", ["Webapps:OfflineCache", @@ -461,7 +465,15 @@ WebappsApplication.prototype = { }, connect: function(aKeyword, aRules) { - // TODO + return this.createPromise(function (aResolver) { + cpmm.sendAsyncMessage("Webapps:Connect", + { keyword: aKeyword, + rules: aRules, + manifestURL: this.manifestURL, + outerWindowID: this._id, + appStatus: this._appStatus, + requestID: this.getPromiseResolverId(aResolver) }); + }.bind(this)); }, getConnections: function() { @@ -487,7 +499,13 @@ WebappsApplication.prototype = { receiveMessage: function(aMessage) { let msg = aMessage.json; - let req = this.takeRequest(msg.requestID); + let req; + if (aMessage.name == "Webapps:Connect:Return:OK" || + aMessage.name == "Webapps:Connect:Return:KO") { + req = this.takePromiseResolver(msg.requestID); + } else { + req = this.takeRequest(msg.requestID); + } // ondownload* callbacks should be triggered on all app instances if ((msg.oid != this._id || !req) && @@ -611,6 +629,18 @@ WebappsApplication.prototype = { case "Webapps:ClearBrowserData:Return": Services.DOMRequest.fireSuccess(req, null); break; + case "Webapps:Connect:Return:OK": + let messagePorts = []; + msg.messagePortIDs.forEach(function(aPortID) { + let port = new this._window.MozInterAppMessagePort(msg.keyword, + aPortID, true); + messagePorts.push(port); + }, this); + req.resolve(messagePorts); + break; + case "Webapps:Connect:Return:KO": + req.reject("No connections registered"); + break; } }, diff --git a/dom/webidl/InterAppMessagePort.webidl b/dom/webidl/InterAppMessagePort.webidl index 13b43bac83b..ad7567cb2b6 100644 --- a/dom/webidl/InterAppMessagePort.webidl +++ b/dom/webidl/InterAppMessagePort.webidl @@ -11,6 +11,7 @@ [HeaderFile="mozilla/dom/InterAppComm.h", Func="mozilla::dom::InterAppComm::EnabledForScope", + Constructor(DOMString keyword, DOMString messagePortID, boolean isPublisher), JSImplementation="@mozilla.org/dom/inter-app-message-port;1"] interface MozInterAppMessagePort : EventTarget { void postMessage(any message);