Bug 876397 - Inter-App Communication API (part 3, connect()). r=nsm

This commit is contained in:
Gene Lian 2013-08-16 17:49:15 +08:00
parent d6a2978a6e
commit 7ee0993f67
5 changed files with 496 additions and 5 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}
},

View File

@ -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
},

View File

@ -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;
}
},

View File

@ -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);