dom/apps/src/InterAppCommService.jsm

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
     9 this.EXPORTED_SYMBOLS = ["InterAppCommService"];
    11 Cu.import("resource://gre/modules/Services.jsm");
    12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    13 Cu.import("resource://gre/modules/AppsUtils.jsm");
    15 const DEBUG = false;
    16 function debug(aMsg) {
    17   dump("-- InterAppCommService: " + Date.now() + ": " + aMsg + "\n");
    18 }
    20 XPCOMUtils.defineLazyServiceGetter(this, "appsService",
    21                                    "@mozilla.org/AppsService;1",
    22                                    "nsIAppsService");
    24 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
    25                                    "@mozilla.org/parentprocessmessagemanager;1",
    26                                    "nsIMessageBroadcaster");
    28 XPCOMUtils.defineLazyServiceGetter(this, "UUIDGenerator",
    29                                    "@mozilla.org/uuid-generator;1",
    30                                    "nsIUUIDGenerator");
    32 XPCOMUtils.defineLazyServiceGetter(this, "messenger",
    33                                    "@mozilla.org/system-message-internal;1",
    34                                    "nsISystemMessagesInternal");
    36 const kMessages =["Webapps:Connect",
    37                   "Webapps:GetConnections",
    38                   "InterAppConnection:Cancel",
    39                   "InterAppMessagePort:PostMessage",
    40                   "InterAppMessagePort:Register",
    41                   "InterAppMessagePort:Unregister",
    42                   "child-process-shutdown"];
    44 /**
    45  * This module contains helpers for Inter-App Communication API [1] related
    46  * purposes, which plays the role of the central service receiving messages
    47  * from and interacting with the content processes.
    48  *
    49  * [1] https://wiki.mozilla.org/WebAPI/Inter_App_Communication_Alt_proposal
    50  */
    52 this.InterAppCommService = {
    53   init: function() {
    54     Services.obs.addObserver(this, "xpcom-shutdown", false);
    55     Services.obs.addObserver(this, "inter-app-comm-select-app-result", false);
    57     kMessages.forEach(function(aMsg) {
    58       ppmm.addMessageListener(aMsg, this);
    59     }, this);
    61     // This matrix is used for saving the inter-app connection info registered in
    62     // the app manifest. The object literal is defined as below:
    63     //
    64     // {
    65     //   "keyword1": {
    66     //     "subAppManifestURL1": {
    67     //       /* subscribed info */
    68     //     },
    69     //     "subAppManifestURL2": {
    70     //       /* subscribed info */
    71     //     },
    72     //     ...
    73     //   },
    74     //   "keyword2": {
    75     //     "subAppManifestURL3": {
    76     //       /* subscribed info */
    77     //     },
    78     //     ...
    79     //   },
    80     //   ...
    81     // }
    82     //
    83     // For example:
    84     //
    85     // {
    86     //   "foo": {
    87     //     "app://subApp1.gaiamobile.org/manifest.webapp": {
    88     //       pageURL: "app://subApp1.gaiamobile.org/handler.html",
    89     //       description: "blah blah",
    90     //       rules: { ... }
    91     //     },
    92     //     "app://subApp2.gaiamobile.org/manifest.webapp": {
    93     //       pageURL: "app://subApp2.gaiamobile.org/handler.html",
    94     //       description: "blah blah",
    95     //       rules: { ... }
    96     //     }
    97     //   },
    98     //   "bar": {
    99     //     "app://subApp3.gaiamobile.org/manifest.webapp": {
   100     //       pageURL: "app://subApp3.gaiamobile.org/handler.html",
   101     //       description: "blah blah",
   102     //       rules: { ... }
   103     //     }
   104     //   }
   105     // }
   106     //
   107     // TODO Bug 908999 - Update registered connections when app gets uninstalled.
   108     this._registeredConnections = {};
   110     // This matrix is used for saving the permitted connections, which allows
   111     // the messaging between publishers and subscribers. The object literal is
   112     // defined as below:
   113     //
   114     // {
   115     //   "keyword1": {
   116     //     "pubAppManifestURL1": [
   117     //       "subAppManifestURL1",
   118     //       "subAppManifestURL2",
   119     //       ...
   120     //     ],
   121     //     "pubAppManifestURL2": [
   122     //       "subAppManifestURL3",
   123     //       "subAppManifestURL4",
   124     //       ...
   125     //     ],
   126     //     ...
   127     //   },
   128     //   "keyword2": {
   129     //     "pubAppManifestURL3": [
   130     //       "subAppManifestURL5",
   131     //       ...
   132     //     ],
   133     //     ...
   134     //   },
   135     //   ...
   136     // }
   137     //
   138     // For example:
   139     //
   140     // {
   141     //   "foo": {
   142     //     "app://pubApp1.gaiamobile.org/manifest.webapp": [
   143     //       "app://subApp1.gaiamobile.org/manifest.webapp",
   144     //       "app://subApp2.gaiamobile.org/manifest.webapp"
   145     //     ],
   146     //     "app://pubApp2.gaiamobile.org/manifest.webapp": [
   147     //       "app://subApp3.gaiamobile.org/manifest.webapp",
   148     //       "app://subApp4.gaiamobile.org/manifest.webapp"
   149     //     ]
   150     //   },
   151     //   "bar": {
   152     //     "app://pubApp3.gaiamobile.org/manifest.webapp": [
   153     //       "app://subApp5.gaiamobile.org/manifest.webapp",
   154     //     ]
   155     //   }
   156     // }
   157     //
   158     // TODO Bug 908999 - Update allowed connections when app gets uninstalled.
   159     this._allowedConnections = {};
   161     // This matrix is used for saving the caller info from the content process,
   162     // which is indexed by a random UUID, to know where to return the promise
   163     // resolvser's callback when the prompt UI for allowing connections returns.
   164     // An example of the object literal is shown as below:
   165     //
   166     // {
   167     //   "fooID": {
   168     //     outerWindowID: 12,
   169     //     requestID: 34,
   170     //     target: pubAppTarget1
   171     //   },
   172     //   "barID": {
   173     //     outerWindowID: 56,
   174     //     requestID: 78,
   175     //     target: pubAppTarget2
   176     //   }
   177     // }
   178     //
   179     // where |outerWindowID| is the ID of the window requesting the connection,
   180     //       |requestID| is the ID specifying the promise resolver to return,
   181     //       |target| is the target of the process requesting the connection.
   182     this._promptUICallers = {};
   184     // This matrix is used for saving the pair of message ports, which is indexed
   185     // by a random UUID, so that each port can know whom it should talk to.
   186     // An example of the object literal is shown as below:
   187     //
   188     // {
   189     //   "UUID1": {
   190     //     keyword: "keyword1",
   191     //     publisher: {
   192     //       manifestURL: "app://pubApp1.gaiamobile.org/manifest.webapp",
   193     //       target: pubAppTarget1,
   194     //       pageURL: "app://pubApp1.gaiamobile.org/caller.html",
   195     //       messageQueue: [...]
   196     //     },
   197     //     subscriber: {
   198     //       manifestURL: "app://subApp1.gaiamobile.org/manifest.webapp",
   199     //       target: subAppTarget1,
   200     //       pageURL: "app://pubApp1.gaiamobile.org/handler.html",
   201     //       messageQueue: [...]
   202     //     }
   203     //   },
   204     //   "UUID2": {
   205     //     keyword: "keyword2",
   206     //     publisher: {
   207     //       manifestURL: "app://pubApp2.gaiamobile.org/manifest.webapp",
   208     //       target: pubAppTarget2,
   209     //       pageURL: "app://pubApp2.gaiamobile.org/caller.html",
   210     //       messageQueue: [...]
   211     //     },
   212     //     subscriber: {
   213     //       manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp",
   214     //       target: subAppTarget2,
   215     //       pageURL: "app://pubApp2.gaiamobile.org/handler.html",
   216     //       messageQueue: [...]
   217     //     }
   218     //   }
   219     // }
   220     this._messagePortPairs = {};
   221   },
   223   /**
   224    * Registration of a page that wants to be connected to other apps through
   225    * the Inter-App Communication API.
   226    *
   227    * @param aKeyword        The connection's keyword.
   228    * @param aHandlerPageURI The URI of the handler's page.
   229    * @param aManifestURI    The webapp's manifest URI.
   230    * @param aDescription    The connection's description.
   231    * @param aRules          The connection's rules.
   232    */
   233   registerConnection: function(aKeyword, aHandlerPageURI, aManifestURI,
   234                                aDescription, aRules) {
   235     let manifestURL = aManifestURI.spec;
   236     let pageURL = aHandlerPageURI.spec;
   238     if (DEBUG) {
   239       debug("registerConnection: aKeyword: " + aKeyword +
   240             " manifestURL: " + manifestURL + " pageURL: " + pageURL +
   241             " aDescription: " + aDescription +
   242             " aRules.minimumAccessLevel: " + aRules.minimumAccessLevel +
   243             " aRules.manifestURLs: " + aRules.manifestURLs +
   244             " aRules.installOrigins: " + aRules.installOrigins);
   245     }
   247     let subAppManifestURLs = this._registeredConnections[aKeyword];
   248     if (!subAppManifestURLs) {
   249       subAppManifestURLs = this._registeredConnections[aKeyword] = {};
   250     }
   252     subAppManifestURLs[manifestURL] = {
   253       pageURL: pageURL,
   254       description: aDescription,
   255       rules: aRules,
   256       manifestURL: manifestURL
   257     };
   258   },
   260   _matchMinimumAccessLevel: function(aRules, aAppStatus) {
   261     if (!aRules || !aRules.minimumAccessLevel) {
   262       if (DEBUG) {
   263         debug("rules.minimumAccessLevel is not available. No need to match.");
   264       }
   265       return true;
   266     }
   268     let minAccessLevel = aRules.minimumAccessLevel;
   269     switch (minAccessLevel) {
   270       case "web":
   271         if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_INSTALLED ||
   272             aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
   273             aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
   274           return true;
   275         }
   276         break;
   277       case "privileged":
   278         if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
   279             aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
   280           return true;
   281         }
   282         break;
   283       case "certified":
   284         if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
   285           return true;
   286         }
   287         break;
   288     }
   290     if (DEBUG) {
   291       debug("rules.minimumAccessLevel is not matched!" +
   292             " minAccessLevel: " + minAccessLevel +
   293             " aAppStatus : " + aAppStatus);
   294     }
   295     return false;
   296   },
   298   _matchManifestURLs: function(aRules, aManifestURL) {
   299     if (!aRules || !Array.isArray(aRules.manifestURLs)) {
   300       if (DEBUG) {
   301         debug("rules.manifestURLs is not available. No need to match.");
   302       }
   303       return true;
   304     }
   306     let manifestURLs = aRules.manifestURLs;
   307     if (manifestURLs.indexOf(aManifestURL) != -1) {
   308       return true;
   309     }
   311     if (DEBUG) {
   312       debug("rules.manifestURLs is not matched!" +
   313             " manifestURLs: " + manifestURLs +
   314             " aManifestURL : " + aManifestURL);
   315     }
   316     return false;
   317   },
   319   _matchInstallOrigins: function(aRules, aInstallOrigin) {
   320     if (!aRules || !Array.isArray(aRules.installOrigins)) {
   321       if (DEBUG) {
   322         debug("rules.installOrigins is not available. No need to match.");
   323       }
   324       return true;
   325     }
   327     let installOrigins = aRules.installOrigins;
   328     if (installOrigins.indexOf(aInstallOrigin) != -1) {
   329       return true;
   330     }
   332     if (DEBUG) {
   333       debug("rules.installOrigins is not matched!" +
   334             " installOrigins: " + installOrigins +
   335             " installOrigin : " + aInstallOrigin);
   336     }
   337     return false;
   338   },
   340   _matchRules: function(aPubAppManifestURL, aPubRules,
   341                         aSubAppManifestURL, aSubRules) {
   342     let pubApp = appsService.getAppByManifestURL(aPubAppManifestURL);
   343     let subApp = appsService.getAppByManifestURL(aSubAppManifestURL);
   345     // TODO Bug 907068 In the initiative step, we only expose this API to
   346     // certified apps to meet the time line. Eventually, we need to make
   347     // it available for the non-certified apps as well. For now, only the
   348     // certified apps can match the rules.
   349     if (pubApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED ||
   350         subApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
   351       if (DEBUG) {
   352         debug("Only certified apps are allowed to do connections.");
   353       }
   354       return false;
   355     }
   357     if (!aPubRules && !aSubRules) {
   358       if (DEBUG) {
   359         debug("No rules for publisher and subscriber. No need to match.");
   360       }
   361       return true;
   362     }
   364     // Check minimumAccessLevel.
   365     if (!this._matchMinimumAccessLevel(aPubRules, subApp.appStatus) ||
   366         !this._matchMinimumAccessLevel(aSubRules, pubApp.appStatus)) {
   367       return false;
   368     }
   370     // Check manifestURLs.
   371     if (!this._matchManifestURLs(aPubRules, aSubAppManifestURL) ||
   372         !this._matchManifestURLs(aSubRules, aPubAppManifestURL)) {
   373       return false;
   374     }
   376     // Check installOrigins.
   377     if (!this._matchInstallOrigins(aPubRules, subApp.installOrigin) ||
   378         !this._matchInstallOrigins(aSubRules, pubApp.installOrigin)) {
   379       return false;
   380     }
   382     // Check developers.
   383     // TODO Do we really want to check this? This one seems naive.
   385     if (DEBUG) debug("All rules are matched.");
   386     return true;
   387   },
   389   _dispatchMessagePorts: function(aKeyword, aPubAppManifestURL,
   390                                   aAllowedSubAppManifestURLs,
   391                                   aTarget, aOuterWindowID, aRequestID) {
   392     if (DEBUG) {
   393       debug("_dispatchMessagePorts: aKeyword: " + aKeyword +
   394             " aPubAppManifestURL: " + aPubAppManifestURL +
   395             " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs);
   396     }
   398     if (aAllowedSubAppManifestURLs.length == 0) {
   399       if (DEBUG) debug("No apps are allowed to connect. Returning.");
   400       aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
   401                                { oid: aOuterWindowID, requestID: aRequestID });
   402       return;
   403     }
   405     let subAppManifestURLs = this._registeredConnections[aKeyword];
   406     if (!subAppManifestURLs) {
   407       if (DEBUG) debug("No apps are subscribed to connect. Returning.");
   408       aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
   409                                { oid: aOuterWindowID, requestID: aRequestID });
   410       return;
   411     }
   413     let messagePortIDs = [];
   414     aAllowedSubAppManifestURLs.forEach(function(aAllowedSubAppManifestURL) {
   415       let subscribedInfo = subAppManifestURLs[aAllowedSubAppManifestURL];
   416       if (!subscribedInfo) {
   417         if (DEBUG) {
   418           debug("The sunscribed info is not available. Skipping: " +
   419                 aAllowedSubAppManifestURL);
   420         }
   421         return;
   422       }
   424       // The message port ID is aimed for identifying the coupling targets
   425       // to deliver messages with each other. This ID is centrally generated
   426       // by the parent and dispatched to both the sender and receiver ends
   427       // for creating their own message ports respectively.
   428       let messagePortID = UUIDGenerator.generateUUID().toString();
   429       this._messagePortPairs[messagePortID] = {
   430         keyword: aKeyword,
   431         publisher: {
   432           manifestURL: aPubAppManifestURL
   433         },
   434         subscriber: {
   435           manifestURL: aAllowedSubAppManifestURL
   436         }
   437       };
   439       // Fire system message to deliver the message port to the subscriber.
   440       messenger.sendMessage("connection",
   441         { keyword: aKeyword,
   442           messagePortID: messagePortID },
   443         Services.io.newURI(subscribedInfo.pageURL, null, null),
   444         Services.io.newURI(subscribedInfo.manifestURL, null, null));
   446       messagePortIDs.push(messagePortID);
   447     }, this);
   449     if (messagePortIDs.length == 0) {
   450       if (DEBUG) debug("No apps are subscribed to connect. Returning.");
   451       aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
   452                                { oid: aOuterWindowID, requestID: aRequestID });
   453       return;
   454     }
   456     // Return the message port IDs to open the message ports for the publisher.
   457     if (DEBUG) debug("messagePortIDs: " + messagePortIDs);
   458     aTarget.sendAsyncMessage("Webapps:Connect:Return:OK",
   459                              { keyword: aKeyword,
   460                                messagePortIDs: messagePortIDs,
   461                                oid: aOuterWindowID, requestID: aRequestID });
   462   },
   464   _connect: function(aMessage, aTarget) {
   465     let keyword = aMessage.keyword;
   466     let pubRules = aMessage.rules;
   467     let pubAppManifestURL = aMessage.manifestURL;
   468     let outerWindowID = aMessage.outerWindowID;
   469     let requestID = aMessage.requestID;
   471     let subAppManifestURLs = this._registeredConnections[keyword];
   472     if (!subAppManifestURLs) {
   473       if (DEBUG) {
   474         debug("No apps are subscribed for this connection. Returning.");
   475       }
   476       this._dispatchMessagePorts(keyword, pubAppManifestURL, [],
   477                                  aTarget, outerWindowID, requestID);
   478       return;
   479     }
   481     // Fetch the apps that used to be allowed to connect before, so that
   482     // users don't need to select/allow them again. That is, we only pop up
   483     // the prompt UI for the *new* connections.
   484     let allowedSubAppManifestURLs = [];
   485     let allowedPubAppManifestURLs = this._allowedConnections[keyword];
   486     if (allowedPubAppManifestURLs &&
   487         allowedPubAppManifestURLs[pubAppManifestURL]) {
   488       allowedSubAppManifestURLs = allowedPubAppManifestURLs[pubAppManifestURL];
   489     }
   491     // Check rules to see if a subscribed app is allowed to connect.
   492     let appsToSelect = [];
   493     for (let subAppManifestURL in subAppManifestURLs) {
   494       if (allowedSubAppManifestURLs.indexOf(subAppManifestURL) != -1) {
   495         if (DEBUG) {
   496           debug("Don't need to select again. Skipping: " + subAppManifestURL);
   497         }
   498         continue;
   499       }
   501       // Only rule-matched publishers/subscribers are allowed to connect.
   502       let subscribedInfo = subAppManifestURLs[subAppManifestURL];
   503       let subRules = subscribedInfo.rules;
   505       let matched =
   506         this._matchRules(pubAppManifestURL, pubRules,
   507                          subAppManifestURL, subRules);
   508       if (!matched) {
   509         if (DEBUG) {
   510           debug("Rules are not matched. Skipping: " + subAppManifestURL);
   511         }
   512         continue;
   513       }
   515       appsToSelect.push({
   516         manifestURL: subAppManifestURL,
   517         description: subscribedInfo.description
   518       });
   519     }
   521     if (appsToSelect.length == 0) {
   522       if (DEBUG) {
   523         debug("No additional apps need to be selected for this connection. " +
   524               "Just dispatch message ports for the existing connections.");
   525       }
   527       this._dispatchMessagePorts(keyword, pubAppManifestURL,
   528                                  allowedSubAppManifestURLs,
   529                                  aTarget, outerWindowID, requestID);
   530       return;
   531     }
   533     // Remember the caller info with an UUID so that we can know where to
   534     // return the promise resolver's callback when the prompt UI returns.
   535     let callerID = UUIDGenerator.generateUUID().toString();
   536     this._promptUICallers[callerID] = {
   537       outerWindowID: outerWindowID,
   538       requestID: requestID,
   539       target: aTarget
   540     };
   542     // TODO Bug 897169 Temporarily disable the notification for popping up
   543     // the prompt until the UX/UI for the prompt is confirmed.
   544     //
   545     // TODO Bug 908191 We need to change the way of interaction between API and
   546     // run-time prompt from observer notification to xpcom-interface caller.
   547     //
   548     /*
   549     if (DEBUG) debug("appsToSelect: " + appsToSelect);
   550     Services.obs.notifyObservers(null, "inter-app-comm-select-app",
   551       JSON.stringify({ callerID: callerID,
   552                        manifestURL: pubAppManifestURL,
   553                        keyword: keyword,
   554                        appsToSelect: appsToSelect }));
   555     */
   557     // TODO Bug 897169 Simulate the return of the app-selected result by
   558     // the prompt, which always allows the connection. This dummy codes
   559     // will be removed when the UX/UI for the prompt is ready.
   560     if (DEBUG) debug("appsToSelect: " + appsToSelect);
   561     Services.obs.notifyObservers(null, 'inter-app-comm-select-app-result',
   562       JSON.stringify({ callerID: callerID,
   563                        manifestURL: pubAppManifestURL,
   564                        keyword: keyword,
   565                        selectedApps: appsToSelect }));
   566   },
   568   _getConnections: function(aMessage, aTarget) {
   569     let outerWindowID = aMessage.outerWindowID;
   570     let requestID = aMessage.requestID;
   572     let connections = [];
   573     for (let keyword in this._allowedConnections) {
   574       let allowedPubAppManifestURLs = this._allowedConnections[keyword];
   575       for (let allowedPubAppManifestURL in allowedPubAppManifestURLs) {
   576         let allowedSubAppManifestURLs =
   577           allowedPubAppManifestURLs[allowedPubAppManifestURL];
   578         allowedSubAppManifestURLs.forEach(function(allowedSubAppManifestURL) {
   579           connections.push({ keyword: keyword,
   580                              pubAppManifestURL: allowedPubAppManifestURL,
   581                              subAppManifestURL: allowedSubAppManifestURL });
   582         });
   583       }
   584     }
   586     aTarget.sendAsyncMessage("Webapps:GetConnections:Return:OK",
   587                              { connections: connections,
   588                                oid: outerWindowID, requestID: requestID });
   589   },
   591   _cancelConnection: function(aMessage) {
   592     let keyword = aMessage.keyword;
   593     let pubAppManifestURL = aMessage.pubAppManifestURL;
   594     let subAppManifestURL = aMessage.subAppManifestURL;
   596     let allowedPubAppManifestURLs = this._allowedConnections[keyword];
   597     if (!allowedPubAppManifestURLs) {
   598       if (DEBUG) debug("keyword is not found: " + keyword);
   599       return;
   600     }
   602     let allowedSubAppManifestURLs =
   603       allowedPubAppManifestURLs[pubAppManifestURL];
   604     if (!allowedSubAppManifestURLs) {
   605       if (DEBUG) debug("publisher is not found: " + pubAppManifestURL);
   606       return;
   607     }
   609     let index = allowedSubAppManifestURLs.indexOf(subAppManifestURL);
   610     if (index == -1) {
   611       if (DEBUG) debug("subscriber is not found: " + subAppManifestURL);
   612       return;
   613     }
   615     if (DEBUG) debug("Cancelling the connection.");
   616     allowedSubAppManifestURLs.splice(index, 1);
   618     // Clean up the parent entries if needed.
   619     if (allowedSubAppManifestURLs.length == 0) {
   620       delete allowedPubAppManifestURLs[pubAppManifestURL];
   621       if (Object.keys(allowedPubAppManifestURLs).length == 0) {
   622         delete this._allowedConnections[keyword];
   623       }
   624     }
   626     if (DEBUG) debug("Unregistering message ports based on this connection.");
   627     let messagePortIDs = [];
   628     for (let messagePortID in this._messagePortPairs) {
   629       let pair = this._messagePortPairs[messagePortID];
   630       if (pair.keyword == keyword &&
   631           pair.publisher.manifestURL == pubAppManifestURL &&
   632           pair.subscriber.manifestURL == subAppManifestURL) {
   633         messagePortIDs.push(messagePortID);
   634       }
   635     }
   636     messagePortIDs.forEach(function(aMessagePortID) {
   637       delete this._messagePortPairs[aMessagePortID];
   638     }, this);
   639   },
   641   _identifyMessagePort: function(aMessagePortID, aManifestURL) {
   642     let pair = this._messagePortPairs[aMessagePortID];
   643     if (!pair) {
   644       if (DEBUG) {
   645         debug("Error! The message port ID is invalid: " + aMessagePortID +
   646               ", which should have been generated by parent.");
   647       }
   648       return null;
   649     }
   651     // Check it the message port is for publisher.
   652     if (pair.publisher.manifestURL == aManifestURL) {
   653       return { pair: pair, isPublisher: true };
   654     }
   656     // Check it the message port is for subscriber.
   657     if (pair.subscriber.manifestURL == aManifestURL) {
   658       return { pair: pair, isPublisher: false };
   659     }
   661     if (DEBUG) {
   662       debug("Error! The manifest URL is invalid: " + aManifestURL +
   663             ", which might be a hacked app.");
   664     }
   665     return null;
   666   },
   668   _registerMessagePort: function(aMessage, aTarget) {
   669     let messagePortID = aMessage.messagePortID;
   670     let manifestURL = aMessage.manifestURL;
   671     let pageURL = aMessage.pageURL;
   673     let identity = this._identifyMessagePort(messagePortID, manifestURL);
   674     if (!identity) {
   675       if (DEBUG) {
   676         debug("Cannot identify the message port. Failed to register.");
   677       }
   678       return;
   679     }
   681     if (DEBUG) debug("Registering message port for " + manifestURL);
   682     let pair = identity.pair;
   683     let isPublisher = identity.isPublisher;
   685     let sender = isPublisher ? pair.publisher : pair.subscriber;
   686     sender.target = aTarget;
   687     sender.pageURL = pageURL;
   688     sender.messageQueue = [];
   690     // Check if the other port has queued messages. Deliver them if needed.
   691     if (DEBUG) {
   692       debug("Checking if the other port used to send messages but queued.");
   693     }
   694     let receiver = isPublisher ? pair.subscriber : pair.publisher;
   695     if (receiver.messageQueue) {
   696       while (receiver.messageQueue.length) {
   697         let message = receiver.messageQueue.shift();
   698         if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
   699         sender.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
   700                                        { message: message,
   701                                          manifestURL: sender.manifestURL,
   702                                          pageURL: sender.pageURL,
   703                                          messagePortID: messagePortID });
   704       }
   705     }
   706   },
   708   _unregisterMessagePort: function(aMessage) {
   709     let messagePortID = aMessage.messagePortID;
   710     let manifestURL = aMessage.manifestURL;
   712     let identity = this._identifyMessagePort(messagePortID, manifestURL);
   713     if (!identity) {
   714       if (DEBUG) {
   715         debug("Cannot identify the message port. Failed to unregister.");
   716       }
   717       return;
   718     }
   720     if (DEBUG) {
   721       debug("Unregistering message port for " + manifestURL);
   722     }
   723     delete this._messagePortPairs[messagePortID];
   724   },
   726   _removeTarget: function(aTarget) {
   727     if (!aTarget) {
   728       if (DEBUG) debug("Error! aTarget cannot be null/undefined in any way.");
   729       return
   730     }
   732     if (DEBUG) debug("Unregistering message ports based on this target.");
   733     let messagePortIDs = [];
   734     for (let messagePortID in this._messagePortPairs) {
   735       let pair = this._messagePortPairs[messagePortID];
   736       if (pair.publisher.target === aTarget ||
   737           pair.subscriber.target === aTarget) {
   738         messagePortIDs.push(messagePortID);
   739       }
   740     }
   741     messagePortIDs.forEach(function(aMessagePortID) {
   742       delete this._messagePortPairs[aMessagePortID];
   743     }, this);
   744   },
   746   _postMessage: function(aMessage) {
   747     let messagePortID = aMessage.messagePortID;
   748     let manifestURL = aMessage.manifestURL;
   749     let message = aMessage.message;
   751     let identity = this._identifyMessagePort(messagePortID, manifestURL);
   752     if (!identity) {
   753       if (DEBUG) debug("Cannot identify the message port. Failed to post.");
   754       return;
   755     }
   757     let pair = identity.pair;
   758     let isPublisher = identity.isPublisher;
   760     let receiver = isPublisher ? pair.subscriber : pair.publisher;
   761     if (!receiver.target) {
   762       if (DEBUG) {
   763         debug("The receiver's target is not ready yet. Queuing the message.");
   764       }
   765       let sender = isPublisher ? pair.publisher : pair.subscriber;
   766       sender.messageQueue.push(message);
   767       return;
   768     }
   770     if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
   771     receiver.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
   772                                      { manifestURL: receiver.manifestURL,
   773                                        pageURL: receiver.pageURL,
   774                                        messagePortID: messagePortID,
   775                                        message: message });
   776   },
   778   _handleSelectcedApps: function(aData) {
   779     let callerID = aData.callerID;
   780     let caller = this._promptUICallers[callerID];
   781     if (!caller) {
   782       if (DEBUG) debug("Error! Cannot find the caller.");
   783       return;
   784     }
   786     delete this._promptUICallers[callerID];
   788     let outerWindowID = caller.outerWindowID;
   789     let requestID = caller.requestID;
   790     let target = caller.target;
   792     let manifestURL = aData.manifestURL;
   793     let keyword = aData.keyword;
   794     let selectedApps = aData.selectedApps;
   796     if (selectedApps.length == 0) {
   797       if (DEBUG) debug("No apps are selected to connect.")
   798       this._dispatchMessagePorts(keyword, manifestURL, [],
   799                                  target, outerWindowID, requestID);
   800       return;
   801     }
   803     // Find the entry of allowed connections to add the selected apps.
   804     let allowedPubAppManifestURLs = this._allowedConnections[keyword];
   805     if (!allowedPubAppManifestURLs) {
   806       allowedPubAppManifestURLs = this._allowedConnections[keyword] = {};
   807     }
   808     let allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL];
   809     if (!allowedSubAppManifestURLs) {
   810       allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL] = [];
   811     }
   813     // Add the selected app into the existing set of allowed connections.
   814     selectedApps.forEach(function(aSelectedApp) {
   815       let allowedSubAppManifestURL = aSelectedApp.manifestURL;
   816       if (allowedSubAppManifestURLs.indexOf(allowedSubAppManifestURL) == -1) {
   817         allowedSubAppManifestURLs.push(allowedSubAppManifestURL);
   818       }
   819     });
   821     // Finally, dispatch the message ports for the allowed connections,
   822     // including the old connections and the newly selected connection.
   823     this._dispatchMessagePorts(keyword, manifestURL, allowedSubAppManifestURLs,
   824                                target, outerWindowID, requestID);
   825   },
   827   receiveMessage: function(aMessage) {
   828     if (DEBUG) debug("receiveMessage: name: " + aMessage.name);
   829     let message = aMessage.json;
   830     let target = aMessage.target;
   832     // To prevent the hacked child process from sending commands to parent
   833     // to do illegal connections, we need to check its manifest URL.
   834     if (aMessage.name !== "child-process-shutdown" &&
   835         // TODO: fix bug 988142 to re-enable "InterAppMessagePort:Unregister".
   836         aMessage.name !== "InterAppMessagePort:Unregister" &&
   837         kMessages.indexOf(aMessage.name) != -1) {
   838       if (!target.assertContainApp(message.manifestURL)) {
   839         if (DEBUG) {
   840           debug("Got message from a process carrying illegal manifest URL.");
   841         }
   842         return null;
   843       }
   844     }
   846     switch (aMessage.name) {
   847       case "Webapps:Connect":
   848         this._connect(message, target);
   849         break;
   850       case "Webapps:GetConnections":
   851         this._getConnections(message, target);
   852         break;
   853       case "InterAppConnection:Cancel":
   854         this._cancelConnection(message);
   855         break;
   856       case "InterAppMessagePort:PostMessage":
   857         this._postMessage(message);
   858         break;
   859       case "InterAppMessagePort:Register":
   860         this._registerMessagePort(message, target);
   861         break;
   862       case "InterAppMessagePort:Unregister":
   863         this._unregisterMessagePort(message);
   864         break;
   865       case "child-process-shutdown":
   866         this._removeTarget(target);
   867         break;
   868     }
   869   },
   871   observe: function(aSubject, aTopic, aData) {
   872     switch (aTopic) {
   873       case "xpcom-shutdown":
   874         Services.obs.removeObserver(this, "xpcom-shutdown");
   875         Services.obs.removeObserver(this, "inter-app-comm-select-app-result");
   876         kMessages.forEach(function(aMsg) {
   877           ppmm.removeMessageListener(aMsg, this);
   878         }, this);
   879         ppmm = null;
   880         break;
   881       case "inter-app-comm-select-app-result":
   882         if (DEBUG) debug("inter-app-comm-select-app-result: " + aData);
   883         this._handleSelectcedApps(JSON.parse(aData));
   884         break;
   885     }
   886   }
   887 };
   889 InterAppCommService.init();

mercurial