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

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

mercurial