зеркало из https://github.com/mozilla/gecko-dev.git
1127 строки
38 KiB
JavaScript
1127 строки
38 KiB
JavaScript
/* 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");
|
|
|
|
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, "webapps-clear-data", 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 = {};
|
|
},
|
|
|
|
/* These attributes main use is to allow testing this in an isolated way
|
|
* that doesn't depend on the app service, or the system messenger working on
|
|
* the test environment
|
|
*/
|
|
get appsService() {
|
|
return this._appsService || appsService;
|
|
},
|
|
set appsService(aService) {
|
|
this._appsService = aService;
|
|
},
|
|
get messenger() {
|
|
return this._messenger || messenger;
|
|
},
|
|
set messenger(aMessenger) {
|
|
this._messenger = aMessenger;
|
|
},
|
|
|
|
|
|
/**
|
|
* 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.pageURLs: " + aRules.pageURLs +
|
|
" 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;
|
|
},
|
|
|
|
_matchPageURLs: function(aRules, aPageURL) {
|
|
|
|
if (!aRules || !aRules.pageURLs) {
|
|
if (DEBUG) {
|
|
debug("rules.pageURLs is not available. No need to match.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!Array.isArray(aRules.pageURLs)) {
|
|
aRules.pageURLs = [aRules.pageURLs];
|
|
}
|
|
|
|
let pageURLs = aRules.pageURLs;
|
|
let isAllowed = false;
|
|
for (let i = 0, li = pageURLs.length; i < li && !isAllowed ; i++) {
|
|
let regExpAllowedURL = new RegExp(pageURLs[i]);
|
|
isAllowed = regExpAllowedURL.test(aPageURL);
|
|
}
|
|
|
|
if (DEBUG) {
|
|
debug("rules.pageURLs is " + (isAllowed ? "" : "not") + " matched!" +
|
|
" pageURLs: " + pageURLs +
|
|
" aPageURL: " + aPageURL);
|
|
}
|
|
|
|
return isAllowed;
|
|
},
|
|
|
|
_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;
|
|
},
|
|
|
|
// A connection is allowed if all the rules are matched.
|
|
// The publisher is matched against the rules defined by the subscriber on the
|
|
// manifest, and the subscriber is matched against the rules defined by the
|
|
// publisher on the call to connect.
|
|
// The possible rules for both subscribers and publishers are:
|
|
// * minimumAccessLevel: "privileged"|"certified"|"web"|undefined
|
|
// The default (non existant or undefined value) is "certified".
|
|
// That means that if an explicit minimumAccessLevel rule does not
|
|
// exist then the peer of the connection *must* be a certified app.
|
|
// * pageURLs: Array of regExp of URLs. If the value exists, only the pages
|
|
// whose URLs are explicitly declared on the array (matched) can connect.
|
|
// Otherwise all pages can connect
|
|
// * installOrigins: Array of origin URLs. If the value exist, only the apps
|
|
// whose origins are on the array can connect. Otherwise, all origins are
|
|
// allowed. This is only checked for non certified apps!
|
|
// The default value (empty or non existant rules) is:
|
|
// * Only certified apps can connect
|
|
// * Any originator/receiving page URLs are valid
|
|
// * Any origin is valid.
|
|
_matchRules: function(aPubAppManifestURL, aPubRules,
|
|
aSubAppManifestURL, aSubRules,
|
|
aPubPageURL, aSubPageURL) {
|
|
let pubApp = this.appsService.getAppByManifestURL(aPubAppManifestURL);
|
|
let subApp = this.appsService.getAppByManifestURL(aSubAppManifestURL);
|
|
|
|
let isPubAppCertified =
|
|
(pubApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED);
|
|
|
|
let isSubAppCertified =
|
|
(subApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED);
|
|
|
|
#ifndef NIGHTLY_BUILD
|
|
|
|
if (!isPubAppCertified || !isSubAppCertified) {
|
|
if (DEBUG) {
|
|
debug("Only certified apps are allowed to do connections.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#else
|
|
|
|
let numSubRules = (aSubRules && Object.keys(aSubRules).length) || 0;
|
|
let numPubRules = (aPubRules && Object.keys(aPubRules).length) || 0;
|
|
|
|
if ((!isSubAppCertified && !numPubRules) ||
|
|
(!isPubAppCertified && !numSubRules)) {
|
|
if (DEBUG) {
|
|
debug("If there aren't rules defined only certified apps are allowed " +
|
|
"to do connections.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endif
|
|
|
|
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 pageURLs.
|
|
if (!this._matchPageURLs(aPubRules, aSubPageURL) ||
|
|
!this._matchPageURLs(aSubRules, aPubPageURL)) {
|
|
return false;
|
|
}
|
|
|
|
// Check installOrigins. Note that we only check the install origin for the
|
|
// non-certified app, because the certified app doesn't have install origin.
|
|
if ((!isSubAppCertified &&
|
|
!this._matchInstallOrigins(aPubRules, subApp.installOrigin)) ||
|
|
(!isPubAppCertified &&
|
|
!this._matchInstallOrigins(aSubRules, pubApp.installOrigin))) {
|
|
return false;
|
|
}
|
|
|
|
if (DEBUG) debug("All rules are matched.");
|
|
return true;
|
|
},
|
|
|
|
_dispatchMessagePorts: function(aKeyword, aPubAppManifestURL,
|
|
aAllowedSubAppManifestURLs,
|
|
aTarget, aOuterWindowID, aRequestID,
|
|
aPubPageURL) {
|
|
if (DEBUG) {
|
|
debug("_dispatchMessagePorts: aKeyword: " + aKeyword +
|
|
" aPubAppManifestURL: " + aPubAppManifestURL +
|
|
" aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs +
|
|
" aPubPageURL: " + aPubPageURL);
|
|
}
|
|
|
|
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.
|
|
this.messenger.sendMessage("connection",
|
|
{ keyword: aKeyword,
|
|
messagePortID: messagePortID,
|
|
pubPageURL: aPubPageURL},
|
|
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 });
|
|
},
|
|
|
|
/**
|
|
* Fetch the subscribers that are currently allowed to connect.
|
|
*
|
|
* @param aKeyword The connection's keyword.
|
|
* @param aPubAppManifestURL The manifest URL of the publisher.
|
|
*
|
|
* @param return an array of manifest URLs of the subscribers.
|
|
*/
|
|
_getAllowedSubAppManifestURLs: function(aKeyword, aPubAppManifestURL) {
|
|
let allowedPubAppManifestURLs = this._allowedConnections[aKeyword];
|
|
if (!allowedPubAppManifestURLs) {
|
|
return [];
|
|
}
|
|
|
|
let allowedSubAppManifestURLs =
|
|
allowedPubAppManifestURLs[aPubAppManifestURL];
|
|
if (!allowedSubAppManifestURLs) {
|
|
return [];
|
|
}
|
|
|
|
return allowedSubAppManifestURLs;
|
|
},
|
|
|
|
/**
|
|
* Add the newly selected apps into the allowed connections and return the
|
|
* aggregated allowed connections.
|
|
*
|
|
* @param aKeyword The connection's keyword.
|
|
* @param aPubAppManifestURL The manifest URL of the publisher.
|
|
* @param aSelectedApps An array of the subscribers' information.
|
|
*
|
|
* @param return an array of manifest URLs of the subscribers.
|
|
*/
|
|
_addSelectedApps: function(aKeyword, aPubAppManifestURL, aSelectedApps) {
|
|
let allowedPubAppManifestURLs = this._allowedConnections[aKeyword];
|
|
|
|
// Add a new entry for |aKeyword|.
|
|
if (!allowedPubAppManifestURLs) {
|
|
allowedPubAppManifestURLs = this._allowedConnections[aKeyword] = {};
|
|
}
|
|
|
|
let allowedSubAppManifestURLs =
|
|
allowedPubAppManifestURLs[aPubAppManifestURL];
|
|
|
|
// Add a new entry for |aPubAppManifestURL|.
|
|
if (!allowedSubAppManifestURLs) {
|
|
allowedSubAppManifestURLs =
|
|
allowedPubAppManifestURLs[aPubAppManifestURL] = [];
|
|
}
|
|
|
|
// Add the selected apps into the existing set of allowed connections.
|
|
aSelectedApps.forEach(function(aSelectedApp) {
|
|
let allowedSubAppManifestURL = aSelectedApp.manifestURL;
|
|
if (allowedSubAppManifestURLs.indexOf(allowedSubAppManifestURL) == -1) {
|
|
allowedSubAppManifestURLs.push(allowedSubAppManifestURL);
|
|
}
|
|
});
|
|
|
|
return allowedSubAppManifestURLs;
|
|
},
|
|
|
|
_connect: function(aMessage, aTarget) {
|
|
let keyword = aMessage.keyword;
|
|
let pubRules = aMessage.rules;
|
|
let pubPageURL = aMessage.pubPageURL;
|
|
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, pubPageURL);
|
|
return;
|
|
}
|
|
|
|
// Fetch the apps that are currently allowed to connect, so that users
|
|
// don't need to select/allow them again, which means we only pop up the
|
|
// prompt UI for the *new* connections.
|
|
let allowedSubAppManifestURLs =
|
|
this._getAllowedSubAppManifestURLs(keyword, 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,
|
|
pubPageURL, subscribedInfo.pageURL);
|
|
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, pubPageURL);
|
|
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
|
|
};
|
|
|
|
let glue = Cc["@mozilla.org/dom/apps/inter-app-comm-ui-glue;1"]
|
|
.createInstance(Ci.nsIInterAppCommUIGlue);
|
|
if (glue) {
|
|
glue.selectApps(callerID, pubAppManifestURL, keyword, appsToSelect).then(
|
|
function(aData) {
|
|
aData.pubPageURL = pubPageURL;
|
|
this._handleSelectedApps(aData);
|
|
}.bind(this),
|
|
function(aError) {
|
|
if (DEBUG) {
|
|
debug("Error occurred in the UI glue component. " + aError);
|
|
}
|
|
|
|
// Resolve the caller as if there were no selected apps.
|
|
this._handleSelectedApps({ callerID: callerID,
|
|
keyword: keyword,
|
|
manifestURL: pubAppManifestURL,
|
|
pubPageURL: pubPageURL,
|
|
selectedApps: [] });
|
|
}.bind(this)
|
|
);
|
|
} else {
|
|
if (DEBUG) {
|
|
debug("Error! The UI glue component is not implemented.");
|
|
}
|
|
|
|
// Resolve the caller as if there were no selected apps.
|
|
this._handleSelectedApps({ callerID: callerID,
|
|
keyword: keyword,
|
|
manifestURL: pubAppManifestURL,
|
|
pubPageURL: pubPageURL,
|
|
selectedApps: [] });
|
|
}
|
|
},
|
|
|
|
_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);
|
|
}
|
|
|
|
let receiver = identity.isPublisher ? identity.pair.subscriber
|
|
: identity.pair.publisher;
|
|
receiver.target.sendAsyncMessage("InterAppMessagePort:OnClose",
|
|
{ manifestURL: receiver.manifestURL,
|
|
pageURL: receiver.pageURL,
|
|
messagePortID: messagePortID });
|
|
|
|
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);
|
|
// Send a shutdown message to the part of the pair that is still alive.
|
|
let actor = pair.publisher.target === aTarget ? pair.subscriber
|
|
: pair.publisher;
|
|
actor.target.sendAsyncMessage("InterAppMessagePort:Shutdown",
|
|
{ manifestURL: actor.manifestURL,
|
|
pageURL: actor.pageURL,
|
|
messagePortID: 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 });
|
|
},
|
|
|
|
_handleSelectedApps: 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 pubAppManifestURL = aData.manifestURL;
|
|
let pubPageURL = aData.pubPageURL;
|
|
let keyword = aData.keyword;
|
|
let selectedApps = aData.selectedApps;
|
|
|
|
let allowedSubAppManifestURLs;
|
|
if (selectedApps.length == 0) {
|
|
// Only do the connections for the existing allowed subscribers because
|
|
// no new apps are selected to connect.
|
|
if (DEBUG) debug("No new apps are selected to connect.");
|
|
|
|
allowedSubAppManifestURLs =
|
|
this._getAllowedSubAppManifestURLs(keyword, pubAppManifestURL);
|
|
} else {
|
|
// Do connections for for the existing allowed subscribers and the newly
|
|
// selected subscribers.
|
|
if (DEBUG) debug("Some new apps are selected to connect.");
|
|
|
|
allowedSubAppManifestURLs =
|
|
this._addSelectedApps(keyword, pubAppManifestURL, selectedApps);
|
|
}
|
|
|
|
// Finally, dispatch the message ports for the allowed connections,
|
|
// including the old connections and the newly selected connection.
|
|
this._dispatchMessagePorts(keyword, pubAppManifestURL,
|
|
allowedSubAppManifestURLs,
|
|
target, outerWindowID, requestID, pubPageURL);
|
|
},
|
|
|
|
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, "webapps-clear-data");
|
|
kMessages.forEach(function(aMsg) {
|
|
ppmm.removeMessageListener(aMsg, this);
|
|
}, this);
|
|
ppmm = null;
|
|
break;
|
|
case "webapps-clear-data":
|
|
let params =
|
|
aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
|
|
if (!params) {
|
|
if (DEBUG) {
|
|
debug("Error updating registered/allowed connections for an " +
|
|
"uninstalled app.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Only update registered/allowed connections for apps.
|
|
if (params.browserOnly) {
|
|
if (DEBUG) {
|
|
debug("Only update registered/allowed connections for apps.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
let manifestURL =
|
|
this.appsService.getManifestURLByLocalId(params.appId);
|
|
if (!manifestURL) {
|
|
if (DEBUG) {
|
|
debug("Error updating registered/allowed connections for an " +
|
|
"uninstalled app.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Update registered connections.
|
|
for (let keyword in this._registeredConnections) {
|
|
let subAppManifestURLs = this._registeredConnections[keyword];
|
|
if (subAppManifestURLs[manifestURL]) {
|
|
delete subAppManifestURLs[manifestURL];
|
|
if (DEBUG) {
|
|
debug("Remove " + manifestURL + " from registered connections " +
|
|
"due to app uninstallation.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update allowed connections.
|
|
for (let keyword in this._allowedConnections) {
|
|
let allowedPubAppManifestURLs = this._allowedConnections[keyword];
|
|
if (allowedPubAppManifestURLs[manifestURL]) {
|
|
delete allowedPubAppManifestURLs[manifestURL];
|
|
if (DEBUG) {
|
|
debug("Remove " + manifestURL + " (as a pub app) from allowed " +
|
|
"connections due to app uninstallation.");
|
|
}
|
|
}
|
|
|
|
for (let pubAppManifestURL in allowedPubAppManifestURLs) {
|
|
let subAppManifestURLs = allowedPubAppManifestURLs[pubAppManifestURL];
|
|
for (let i = subAppManifestURLs.length - 1; i >= 0; i--) {
|
|
if (subAppManifestURLs[i] === manifestURL) {
|
|
subAppManifestURLs.splice(i, 1);
|
|
if (DEBUG) {
|
|
debug("Remove " + manifestURL + " (as a sub app to pub " +
|
|
pubAppManifestURL + ") from allowed connections " +
|
|
"due to app uninstallation.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
debug("Finish updating registered/allowed connections for an " +
|
|
"uninstalled app.");
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
InterAppCommService.init();
|