Wed, 31 Dec 2014 06:55:50 +0100
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();