Bumping manifests a=b2g-bump
[gecko.git] / dom / apps / InterAppCommService.jsm
blob4da7d5e901cce5f2d7fa252501278a896b3e99c0
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");
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, "webapps-clear-data", 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     let isPubAppCertified =
346       (pubApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED);
348     let isSubAppCertified =
349       (subApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED);
351     // TODO Bug 907068 In the initiative step, we only expose this API to
352     // certified apps to meet the time line. Eventually, we need to make
353     // it available for the non-certified apps as well. For now, only the
354     // certified apps can match the rules.
355     if (!isPubAppCertified || !isSubAppCertified) {
356       if (DEBUG) {
357         debug("Only certified apps are allowed to do connections.");
358       }
359       return false;
360     }
362     if (!aPubRules && !aSubRules) {
363       if (DEBUG) {
364         debug("No rules for publisher and subscriber. No need to match.");
365       }
366       return true;
367     }
369     // Check minimumAccessLevel.
370     if (!this._matchMinimumAccessLevel(aPubRules, subApp.appStatus) ||
371         !this._matchMinimumAccessLevel(aSubRules, pubApp.appStatus)) {
372       return false;
373     }
375     // Check manifestURLs.
376     if (!this._matchManifestURLs(aPubRules, aSubAppManifestURL) ||
377         !this._matchManifestURLs(aSubRules, aPubAppManifestURL)) {
378       return false;
379     }
381     // Check installOrigins. Note that we only check the install origin for the
382     // non-certified app, because the certified app doesn't have install origin.
383     if ((!isSubAppCertified &&
384          !this._matchInstallOrigins(aPubRules, subApp.installOrigin)) ||
385         (!isPubAppCertified &&
386          !this._matchInstallOrigins(aSubRules, pubApp.installOrigin))) {
387       return false;
388     }
390     if (DEBUG) debug("All rules are matched.");
391     return true;
392   },
394   _dispatchMessagePorts: function(aKeyword, aPubAppManifestURL,
395                                   aAllowedSubAppManifestURLs,
396                                   aTarget, aOuterWindowID, aRequestID) {
397     if (DEBUG) {
398       debug("_dispatchMessagePorts: aKeyword: " + aKeyword +
399             " aPubAppManifestURL: " + aPubAppManifestURL +
400             " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs);
401     }
403     if (aAllowedSubAppManifestURLs.length == 0) {
404       if (DEBUG) debug("No apps are allowed to connect. Returning.");
405       aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
406                                { oid: aOuterWindowID, requestID: aRequestID });
407       return;
408     }
410     let subAppManifestURLs = this._registeredConnections[aKeyword];
411     if (!subAppManifestURLs) {
412       if (DEBUG) debug("No apps are subscribed to connect. Returning.");
413       aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
414                                { oid: aOuterWindowID, requestID: aRequestID });
415       return;
416     }
418     let messagePortIDs = [];
419     aAllowedSubAppManifestURLs.forEach(function(aAllowedSubAppManifestURL) {
420       let subscribedInfo = subAppManifestURLs[aAllowedSubAppManifestURL];
421       if (!subscribedInfo) {
422         if (DEBUG) {
423           debug("The sunscribed info is not available. Skipping: " +
424                 aAllowedSubAppManifestURL);
425         }
426         return;
427       }
429       // The message port ID is aimed for identifying the coupling targets
430       // to deliver messages with each other. This ID is centrally generated
431       // by the parent and dispatched to both the sender and receiver ends
432       // for creating their own message ports respectively.
433       let messagePortID = UUIDGenerator.generateUUID().toString();
434       this._messagePortPairs[messagePortID] = {
435         keyword: aKeyword,
436         publisher: {
437           manifestURL: aPubAppManifestURL
438         },
439         subscriber: {
440           manifestURL: aAllowedSubAppManifestURL
441         }
442       };
444       // Fire system message to deliver the message port to the subscriber.
445       messenger.sendMessage("connection",
446         { keyword: aKeyword,
447           messagePortID: messagePortID },
448         Services.io.newURI(subscribedInfo.pageURL, null, null),
449         Services.io.newURI(subscribedInfo.manifestURL, null, null));
451       messagePortIDs.push(messagePortID);
452     }, this);
454     if (messagePortIDs.length == 0) {
455       if (DEBUG) debug("No apps are subscribed to connect. Returning.");
456       aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
457                                { oid: aOuterWindowID, requestID: aRequestID });
458       return;
459     }
461     // Return the message port IDs to open the message ports for the publisher.
462     if (DEBUG) debug("messagePortIDs: " + messagePortIDs);
463     aTarget.sendAsyncMessage("Webapps:Connect:Return:OK",
464                              { keyword: aKeyword,
465                                messagePortIDs: messagePortIDs,
466                                oid: aOuterWindowID, requestID: aRequestID });
467   },
469   /**
470    * Fetch the subscribers that are currently allowed to connect.
471    *
472    * @param aKeyword           The connection's keyword.
473    * @param aPubAppManifestURL The manifest URL of the publisher.
474    *
475    * @param return an array of manifest URLs of the subscribers.
476    */
477   _getAllowedSubAppManifestURLs: function(aKeyword, aPubAppManifestURL) {
478     let allowedPubAppManifestURLs = this._allowedConnections[aKeyword];
479     if (!allowedPubAppManifestURLs) {
480       return [];
481     }
483     let allowedSubAppManifestURLs =
484       allowedPubAppManifestURLs[aPubAppManifestURL];
485     if (!allowedSubAppManifestURLs) {
486       return [];
487     }
489     return allowedSubAppManifestURLs;
490   },
492   /**
493    * Add the newly selected apps into the allowed connections and return the
494    * aggregated allowed connections.
495    *
496    * @param aKeyword           The connection's keyword.
497    * @param aPubAppManifestURL The manifest URL of the publisher.
498    * @param aSelectedApps      An array of the subscribers' information.
499    *
500    * @param return an array of manifest URLs of the subscribers.
501    */
502   _addSelectedApps: function(aKeyword, aPubAppManifestURL, aSelectedApps) {
503     let allowedPubAppManifestURLs = this._allowedConnections[aKeyword];
505     // Add a new entry for |aKeyword|.
506     if (!allowedPubAppManifestURLs) {
507       allowedPubAppManifestURLs = this._allowedConnections[aKeyword] = {};
508     }
510     let allowedSubAppManifestURLs =
511       allowedPubAppManifestURLs[aPubAppManifestURL];
513     // Add a new entry for |aPubAppManifestURL|.
514     if (!allowedSubAppManifestURLs) {
515       allowedSubAppManifestURLs =
516         allowedPubAppManifestURLs[aPubAppManifestURL] = [];
517     }
519     // Add the selected apps into the existing set of allowed connections.
520     aSelectedApps.forEach(function(aSelectedApp) {
521       let allowedSubAppManifestURL = aSelectedApp.manifestURL;
522       if (allowedSubAppManifestURLs.indexOf(allowedSubAppManifestURL) == -1) {
523         allowedSubAppManifestURLs.push(allowedSubAppManifestURL);
524       }
525     });
527     return allowedSubAppManifestURLs;
528   },
530   _connect: function(aMessage, aTarget) {
531     let keyword = aMessage.keyword;
532     let pubRules = aMessage.rules;
533     let pubAppManifestURL = aMessage.manifestURL;
534     let outerWindowID = aMessage.outerWindowID;
535     let requestID = aMessage.requestID;
537     let subAppManifestURLs = this._registeredConnections[keyword];
538     if (!subAppManifestURLs) {
539       if (DEBUG) {
540         debug("No apps are subscribed for this connection. Returning.");
541       }
542       this._dispatchMessagePorts(keyword, pubAppManifestURL, [],
543                                  aTarget, outerWindowID, requestID);
544       return;
545     }
547     // Fetch the apps that are currently allowed to connect, so that users
548     // don't need to select/allow them again, which means we only pop up the
549     // prompt UI for the *new* connections.
550     let allowedSubAppManifestURLs =
551       this._getAllowedSubAppManifestURLs(keyword, pubAppManifestURL);
553     // Check rules to see if a subscribed app is allowed to connect.
554     let appsToSelect = [];
555     for (let subAppManifestURL in subAppManifestURLs) {
556       if (allowedSubAppManifestURLs.indexOf(subAppManifestURL) != -1) {
557         if (DEBUG) {
558           debug("Don't need to select again. Skipping: " + subAppManifestURL);
559         }
560         continue;
561       }
563       // Only rule-matched publishers/subscribers are allowed to connect.
564       let subscribedInfo = subAppManifestURLs[subAppManifestURL];
565       let subRules = subscribedInfo.rules;
567       let matched =
568         this._matchRules(pubAppManifestURL, pubRules,
569                          subAppManifestURL, subRules);
570       if (!matched) {
571         if (DEBUG) {
572           debug("Rules are not matched. Skipping: " + subAppManifestURL);
573         }
574         continue;
575       }
577       appsToSelect.push({
578         manifestURL: subAppManifestURL,
579         description: subscribedInfo.description
580       });
581     }
583     if (appsToSelect.length == 0) {
584       if (DEBUG) {
585         debug("No additional apps need to be selected for this connection. " +
586               "Just dispatch message ports for the existing connections.");
587       }
589       this._dispatchMessagePorts(keyword, pubAppManifestURL,
590                                  allowedSubAppManifestURLs,
591                                  aTarget, outerWindowID, requestID);
592       return;
593     }
595     // Remember the caller info with an UUID so that we can know where to
596     // return the promise resolver's callback when the prompt UI returns.
597     let callerID = UUIDGenerator.generateUUID().toString();
598     this._promptUICallers[callerID] = {
599       outerWindowID: outerWindowID,
600       requestID: requestID,
601       target: aTarget
602     };
604     let glue = Cc["@mozilla.org/dom/apps/inter-app-comm-ui-glue;1"]
605                  .createInstance(Ci.nsIInterAppCommUIGlue);
606     if (glue) {
607       glue.selectApps(callerID, pubAppManifestURL, keyword, appsToSelect).then(
608         function(aData) {
609           this._handleSelectedApps(aData);
610         }.bind(this),
611         function(aError) {
612           if (DEBUG) {
613             debug("Error occurred in the UI glue component. " + aError)
614           }
616           // Resolve the caller as if there were no selected apps.
617           this._handleSelectedApps({ callerID: callerID,
618                                      keyword: keyword,
619                                      manifestURL: pubAppManifestURL,
620                                      selectedApps: [] });
621         }.bind(this)
622       );
623     } else {
624       if (DEBUG) {
625         debug("Error! The UI glue component is not implemented.")
626       }
628       // Resolve the caller as if there were no selected apps.
629       this._handleSelectedApps({ callerID: callerID,
630                                  keyword: keyword,
631                                  manifestURL: pubAppManifestURL,
632                                  selectedApps: [] });
633     }
634   },
636   _getConnections: function(aMessage, aTarget) {
637     let outerWindowID = aMessage.outerWindowID;
638     let requestID = aMessage.requestID;
640     let connections = [];
641     for (let keyword in this._allowedConnections) {
642       let allowedPubAppManifestURLs = this._allowedConnections[keyword];
643       for (let allowedPubAppManifestURL in allowedPubAppManifestURLs) {
644         let allowedSubAppManifestURLs =
645           allowedPubAppManifestURLs[allowedPubAppManifestURL];
646         allowedSubAppManifestURLs.forEach(function(allowedSubAppManifestURL) {
647           connections.push({ keyword: keyword,
648                              pubAppManifestURL: allowedPubAppManifestURL,
649                              subAppManifestURL: allowedSubAppManifestURL });
650         });
651       }
652     }
654     aTarget.sendAsyncMessage("Webapps:GetConnections:Return:OK",
655                              { connections: connections,
656                                oid: outerWindowID, requestID: requestID });
657   },
659   _cancelConnection: function(aMessage) {
660     let keyword = aMessage.keyword;
661     let pubAppManifestURL = aMessage.pubAppManifestURL;
662     let subAppManifestURL = aMessage.subAppManifestURL;
664     let allowedPubAppManifestURLs = this._allowedConnections[keyword];
665     if (!allowedPubAppManifestURLs) {
666       if (DEBUG) debug("keyword is not found: " + keyword);
667       return;
668     }
670     let allowedSubAppManifestURLs =
671       allowedPubAppManifestURLs[pubAppManifestURL];
672     if (!allowedSubAppManifestURLs) {
673       if (DEBUG) debug("publisher is not found: " + pubAppManifestURL);
674       return;
675     }
677     let index = allowedSubAppManifestURLs.indexOf(subAppManifestURL);
678     if (index == -1) {
679       if (DEBUG) debug("subscriber is not found: " + subAppManifestURL);
680       return;
681     }
683     if (DEBUG) debug("Cancelling the connection.");
684     allowedSubAppManifestURLs.splice(index, 1);
686     // Clean up the parent entries if needed.
687     if (allowedSubAppManifestURLs.length == 0) {
688       delete allowedPubAppManifestURLs[pubAppManifestURL];
689       if (Object.keys(allowedPubAppManifestURLs).length == 0) {
690         delete this._allowedConnections[keyword];
691       }
692     }
694     if (DEBUG) debug("Unregistering message ports based on this connection.");
695     let messagePortIDs = [];
696     for (let messagePortID in this._messagePortPairs) {
697       let pair = this._messagePortPairs[messagePortID];
698       if (pair.keyword == keyword &&
699           pair.publisher.manifestURL == pubAppManifestURL &&
700           pair.subscriber.manifestURL == subAppManifestURL) {
701         messagePortIDs.push(messagePortID);
702       }
703     }
704     messagePortIDs.forEach(function(aMessagePortID) {
705       delete this._messagePortPairs[aMessagePortID];
706     }, this);
707   },
709   _identifyMessagePort: function(aMessagePortID, aManifestURL) {
710     let pair = this._messagePortPairs[aMessagePortID];
711     if (!pair) {
712       if (DEBUG) {
713         debug("Error! The message port ID is invalid: " + aMessagePortID +
714               ", which should have been generated by parent.");
715       }
716       return null;
717     }
719     // Check it the message port is for publisher.
720     if (pair.publisher.manifestURL == aManifestURL) {
721       return { pair: pair, isPublisher: true };
722     }
724     // Check it the message port is for subscriber.
725     if (pair.subscriber.manifestURL == aManifestURL) {
726       return { pair: pair, isPublisher: false };
727     }
729     if (DEBUG) {
730       debug("Error! The manifest URL is invalid: " + aManifestURL +
731             ", which might be a hacked app.");
732     }
733     return null;
734   },
736   _registerMessagePort: function(aMessage, aTarget) {
737     let messagePortID = aMessage.messagePortID;
738     let manifestURL = aMessage.manifestURL;
739     let pageURL = aMessage.pageURL;
741     let identity = this._identifyMessagePort(messagePortID, manifestURL);
742     if (!identity) {
743       if (DEBUG) {
744         debug("Cannot identify the message port. Failed to register.");
745       }
746       return;
747     }
749     if (DEBUG) debug("Registering message port for " + manifestURL);
750     let pair = identity.pair;
751     let isPublisher = identity.isPublisher;
753     let sender = isPublisher ? pair.publisher : pair.subscriber;
754     sender.target = aTarget;
755     sender.pageURL = pageURL;
756     sender.messageQueue = [];
758     // Check if the other port has queued messages. Deliver them if needed.
759     if (DEBUG) {
760       debug("Checking if the other port used to send messages but queued.");
761     }
762     let receiver = isPublisher ? pair.subscriber : pair.publisher;
763     if (receiver.messageQueue) {
764       while (receiver.messageQueue.length) {
765         let message = receiver.messageQueue.shift();
766         if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
767         sender.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
768                                        { message: message,
769                                          manifestURL: sender.manifestURL,
770                                          pageURL: sender.pageURL,
771                                          messagePortID: messagePortID });
772       }
773     }
774   },
776   _unregisterMessagePort: function(aMessage) {
777     let messagePortID = aMessage.messagePortID;
778     let manifestURL = aMessage.manifestURL;
780     let identity = this._identifyMessagePort(messagePortID, manifestURL);
781     if (!identity) {
782       if (DEBUG) {
783         debug("Cannot identify the message port. Failed to unregister.");
784       }
785       return;
786     }
788     if (DEBUG) {
789       debug("Unregistering message port for " + manifestURL);
790     }
791     delete this._messagePortPairs[messagePortID];
792   },
794   _removeTarget: function(aTarget) {
795     if (!aTarget) {
796       if (DEBUG) debug("Error! aTarget cannot be null/undefined in any way.");
797       return
798     }
800     if (DEBUG) debug("Unregistering message ports based on this target.");
801     let messagePortIDs = [];
802     for (let messagePortID in this._messagePortPairs) {
803       let pair = this._messagePortPairs[messagePortID];
804       if (pair.publisher.target === aTarget ||
805           pair.subscriber.target === aTarget) {
806         messagePortIDs.push(messagePortID);
807         // Send a shutdown message to the part of the pair that is still alive.
808         let actor = pair.publisher.target === aTarget ? pair.subscriber
809                                                        : pair.publisher;
810         actor.target.sendAsyncMessage("InterAppMessagePort:Shutdown",
811           { manifestURL: actor.manifestURL,
812             pageURL: actor.pageURL,
813             messagePortID: messagePortID });
814       }
815     }
816     messagePortIDs.forEach(function(aMessagePortID) {
817       delete this._messagePortPairs[aMessagePortID];
818     }, this);
819   },
821   _postMessage: function(aMessage) {
822     let messagePortID = aMessage.messagePortID;
823     let manifestURL = aMessage.manifestURL;
824     let message = aMessage.message;
826     let identity = this._identifyMessagePort(messagePortID, manifestURL);
827     if (!identity) {
828       if (DEBUG) debug("Cannot identify the message port. Failed to post.");
829       return;
830     }
832     let pair = identity.pair;
833     let isPublisher = identity.isPublisher;
835     let receiver = isPublisher ? pair.subscriber : pair.publisher;
836     if (!receiver.target) {
837       if (DEBUG) {
838         debug("The receiver's target is not ready yet. Queuing the message.");
839       }
840       let sender = isPublisher ? pair.publisher : pair.subscriber;
841       sender.messageQueue.push(message);
842       return;
843     }
845     if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
846     receiver.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
847                                      { manifestURL: receiver.manifestURL,
848                                        pageURL: receiver.pageURL,
849                                        messagePortID: messagePortID,
850                                        message: message });
851   },
853   _handleSelectedApps: function(aData) {
854     let callerID = aData.callerID;
855     let caller = this._promptUICallers[callerID];
856     if (!caller) {
857       if (DEBUG) debug("Error! Cannot find the caller.");
858       return;
859     }
861     delete this._promptUICallers[callerID];
863     let outerWindowID = caller.outerWindowID;
864     let requestID = caller.requestID;
865     let target = caller.target;
867     let pubAppManifestURL = aData.manifestURL;
868     let keyword = aData.keyword;
869     let selectedApps = aData.selectedApps;
871     let allowedSubAppManifestURLs;
872     if (selectedApps.length == 0) {
873       // Only do the connections for the existing allowed subscribers because
874       // no new apps are selected to connect.
875       if (DEBUG) debug("No new apps are selected to connect.")
877       allowedSubAppManifestURLs =
878         this._getAllowedSubAppManifestURLs(keyword, pubAppManifestURL);
879     } else {
880       // Do connections for for the existing allowed subscribers and the newly
881       // selected subscribers.
882       if (DEBUG) debug("Some new apps are selected to connect.")
884       allowedSubAppManifestURLs =
885         this._addSelectedApps(keyword, pubAppManifestURL, selectedApps);
886     }
888     // Finally, dispatch the message ports for the allowed connections,
889     // including the old connections and the newly selected connection.
890     this._dispatchMessagePorts(keyword, pubAppManifestURL,
891                                allowedSubAppManifestURLs,
892                                target, outerWindowID, requestID);
893   },
895   receiveMessage: function(aMessage) {
896     if (DEBUG) debug("receiveMessage: name: " + aMessage.name);
897     let message = aMessage.json;
898     let target = aMessage.target;
900     // To prevent the hacked child process from sending commands to parent
901     // to do illegal connections, we need to check its manifest URL.
902     if (aMessage.name !== "child-process-shutdown" &&
903         // TODO: fix bug 988142 to re-enable "InterAppMessagePort:Unregister".
904         aMessage.name !== "InterAppMessagePort:Unregister" &&
905         kMessages.indexOf(aMessage.name) != -1) {
906       if (!target.assertContainApp(message.manifestURL)) {
907         if (DEBUG) {
908           debug("Got message from a process carrying illegal manifest URL.");
909         }
910         return null;
911       }
912     }
914     switch (aMessage.name) {
915       case "Webapps:Connect":
916         this._connect(message, target);
917         break;
918       case "Webapps:GetConnections":
919         this._getConnections(message, target);
920         break;
921       case "InterAppConnection:Cancel":
922         this._cancelConnection(message);
923         break;
924       case "InterAppMessagePort:PostMessage":
925         this._postMessage(message);
926         break;
927       case "InterAppMessagePort:Register":
928         this._registerMessagePort(message, target);
929         break;
930       case "InterAppMessagePort:Unregister":
931         this._unregisterMessagePort(message);
932         break;
933       case "child-process-shutdown":
934         this._removeTarget(target);
935         break;
936     }
937   },
939   observe: function(aSubject, aTopic, aData) {
940     switch (aTopic) {
941       case "xpcom-shutdown":
942         Services.obs.removeObserver(this, "xpcom-shutdown");
943         Services.obs.removeObserver(this, "webapps-clear-data");
944         kMessages.forEach(function(aMsg) {
945           ppmm.removeMessageListener(aMsg, this);
946         }, this);
947         ppmm = null;
948         break;
949       case "webapps-clear-data":
950         let params =
951           aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
952         if (!params) {
953           if (DEBUG) {
954             debug("Error updating registered/allowed connections for an " +
955                   "uninstalled app.");
956           }
957           return;
958         }
960         // Only update registered/allowed connections for apps.
961         if (params.browserOnly) {
962           if (DEBUG) {
963             debug("Only update registered/allowed connections for apps.");
964           }
965           return;
966         }
968         let manifestURL = appsService.getManifestURLByLocalId(params.appId);
969         if (!manifestURL) {
970           if (DEBUG) {
971             debug("Error updating registered/allowed connections for an " +
972                   "uninstalled app.");
973           }
974           return;
975         }
977         // Update registered connections.
978         for (let keyword in this._registeredConnections) {
979           let subAppManifestURLs = this._registeredConnections[keyword];
980           if (subAppManifestURLs[manifestURL]) {
981             delete subAppManifestURLs[manifestURL];
982             if (DEBUG) {
983               debug("Remove " + manifestURL + " from registered connections " +
984                     "due to app uninstallation.");
985             }
986           }
987         }
989         // Update allowed connections.
990         for (let keyword in this._allowedConnections) {
991           let allowedPubAppManifestURLs = this._allowedConnections[keyword];
992           if (allowedPubAppManifestURLs[manifestURL]) {
993             delete allowedPubAppManifestURLs[manifestURL];
994             if (DEBUG) {
995               debug("Remove " + manifestURL + " (as a pub app) from allowed " +
996                     "connections due to app uninstallation.");
997             }
998           }
1000           for (let pubAppManifestURL in allowedPubAppManifestURLs) {
1001             let subAppManifestURLs = allowedPubAppManifestURLs[pubAppManifestURL];
1002             for (let i = subAppManifestURLs.length - 1; i >= 0; i--) {
1003               if (subAppManifestURLs[i] === manifestURL) {
1004                 subAppManifestURLs.splice(i, 1);
1005                 if (DEBUG) {
1006                   debug("Remove " + manifestURL + " (as a sub app to pub " +
1007                         pubAppManifestURL + ") from allowed connections " +
1008                         "due to app uninstallation.");
1009                 }
1010               }
1011             }
1012           }
1013         }
1014         debug("Finish updating registered/allowed connections for an " +
1015               "uninstalled app.");
1016         break;
1017     }
1018   }
1021 InterAppCommService.init();