Home | History | Annotate | Download | only in resources
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 // chrome.runtime.messaging API implementation.
      6 
      7   // TODO(kalman): factor requiring chrome out of here.
      8   var chrome = requireNative('chrome').GetChrome();
      9   var Event = require('event_bindings').Event;
     10   var lastError = require('lastError');
     11   var logActivity = requireNative('activityLogger');
     12   var logging = requireNative('logging');
     13   var messagingNatives = requireNative('messaging_natives');
     14   var processNatives = requireNative('process');
     15   var unloadEvent = require('unload_event');
     16   var utils = require('utils');
     17   var messagingUtils = require('messaging_utils');
     18 
     19   // The reserved channel name for the sendRequest/send(Native)Message APIs.
     20   // Note: sendRequest is deprecated.
     21   var kRequestChannel = "chrome.extension.sendRequest";
     22   var kMessageChannel = "chrome.runtime.sendMessage";
     23   var kNativeMessageChannel = "chrome.runtime.sendNativeMessage";
     24 
     25   // Map of port IDs to port object.
     26   var ports = {};
     27 
     28   // Map of port IDs to unloadEvent listeners. Keep track of these to free the
     29   // unloadEvent listeners when ports are closed.
     30   var portReleasers = {};
     31 
     32   // Change even to odd and vice versa, to get the other side of a given
     33   // channel.
     34   function getOppositePortId(portId) { return portId ^ 1; }
     35 
     36   // Port object.  Represents a connection to another script context through
     37   // which messages can be passed.
     38   function PortImpl(portId, opt_name) {
     39     this.portId_ = portId;
     40     this.name = opt_name;
     41 
     42     var portSchema = {name: 'port', $ref: 'runtime.Port'};
     43     var options = {unmanaged: true};
     44     this.onDisconnect = new Event(null, [portSchema], options);
     45     this.onMessage = new Event(
     46         null,
     47         [{name: 'message', type: 'any', optional: true}, portSchema],
     48         options);
     49     this.onDestroy_ = null;
     50   }
     51 
     52   // Sends a message asynchronously to the context on the other end of this
     53   // port.
     54   PortImpl.prototype.postMessage = function(msg) {
     55     // JSON.stringify doesn't support a root object which is undefined.
     56     if (msg === undefined)
     57       msg = null;
     58     msg = $JSON.stringify(msg);
     59     if (msg === undefined) {
     60       // JSON.stringify can fail with unserializable objects. Log an error and
     61       // drop the message.
     62       //
     63       // TODO(kalman/mpcomplete): it would be better to do the same validation
     64       // here that we do for runtime.sendMessage (and variants), i.e. throw an
     65       // schema validation Error, but just maintain the old behaviour until
     66       // there's a good reason not to (http://crbug.com/263077).
     67       console.error('Illegal argument to Port.postMessage');
     68       return;
     69     }
     70     messagingNatives.PostMessage(this.portId_, msg);
     71   };
     72 
     73   // Disconnects the port from the other end.
     74   PortImpl.prototype.disconnect = function() {
     75     messagingNatives.CloseChannel(this.portId_, true);
     76     this.destroy_();
     77   };
     78 
     79   PortImpl.prototype.destroy_ = function() {
     80     var portId = this.portId_;
     81 
     82     if (this.onDestroy_)
     83       this.onDestroy_();
     84     privates(this.onDisconnect).impl.destroy_();
     85     privates(this.onMessage).impl.destroy_();
     86 
     87     messagingNatives.PortRelease(portId);
     88     unloadEvent.removeListener(portReleasers[portId]);
     89 
     90     delete ports[portId];
     91     delete portReleasers[portId];
     92   };
     93 
     94   // Returns true if the specified port id is in this context. This is used by
     95   // the C++ to avoid creating the javascript message for all the contexts that
     96   // don't care about a particular message.
     97   function hasPort(portId) {
     98     return portId in ports;
     99   };
    100 
    101   // Hidden port creation function.  We don't want to expose an API that lets
    102   // people add arbitrary port IDs to the port list.
    103   function createPort(portId, opt_name) {
    104     if (ports[portId])
    105       throw new Error("Port '" + portId + "' already exists.");
    106     var port = new Port(portId, opt_name);
    107     ports[portId] = port;
    108     portReleasers[portId] = $Function.bind(messagingNatives.PortRelease,
    109                                            this,
    110                                            portId);
    111     unloadEvent.addListener(portReleasers[portId]);
    112     messagingNatives.PortAddRef(portId);
    113     return port;
    114   };
    115 
    116   // Helper function for dispatchOnRequest.
    117   function handleSendRequestError(isSendMessage,
    118                                   responseCallbackPreserved,
    119                                   sourceExtensionId,
    120                                   targetExtensionId,
    121                                   sourceUrl) {
    122     var errorMsg = [];
    123     var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
    124     if (isSendMessage && !responseCallbackPreserved) {
    125       $Array.push(errorMsg,
    126           "The chrome." + eventName + " listener must return true if you " +
    127           "want to send a response after the listener returns");
    128     } else {
    129       $Array.push(errorMsg,
    130           "Cannot send a response more than once per chrome." + eventName +
    131           " listener per document");
    132     }
    133     $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId);
    134     if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId)
    135       $Array.push(errorMsg, "for extension " + targetExtensionId);
    136     if (sourceUrl != "")
    137       $Array.push(errorMsg, "for URL " + sourceUrl);
    138     lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome);
    139   }
    140 
    141   // Helper function for dispatchOnConnect
    142   function dispatchOnRequest(portId, channelName, sender,
    143                              sourceExtensionId, targetExtensionId, sourceUrl,
    144                              isExternal) {
    145     var isSendMessage = channelName == kMessageChannel;
    146     var requestEvent = null;
    147     if (isSendMessage) {
    148       if (chrome.runtime) {
    149         requestEvent = isExternal ? chrome.runtime.onMessageExternal
    150                                   : chrome.runtime.onMessage;
    151       }
    152     } else {
    153       if (chrome.extension) {
    154         requestEvent = isExternal ? chrome.extension.onRequestExternal
    155                                   : chrome.extension.onRequest;
    156       }
    157     }
    158     if (!requestEvent)
    159       return false;
    160     if (!requestEvent.hasListeners())
    161       return false;
    162     var port = createPort(portId, channelName);
    163 
    164     function messageListener(request) {
    165       var responseCallbackPreserved = false;
    166       var responseCallback = function(response) {
    167         if (port) {
    168           port.postMessage(response);
    169           privates(port).impl.destroy_();
    170           port = null;
    171         } else {
    172           // We nulled out port when sending the response, and now the page
    173           // is trying to send another response for the same request.
    174           handleSendRequestError(isSendMessage, responseCallbackPreserved,
    175                                  sourceExtensionId, targetExtensionId);
    176         }
    177       };
    178       // In case the extension never invokes the responseCallback, and also
    179       // doesn't keep a reference to it, we need to clean up the port. Do
    180       // so by attaching to the garbage collection of the responseCallback
    181       // using some native hackery.
    182       messagingNatives.BindToGC(responseCallback, function() {
    183         if (port) {
    184           privates(port).impl.destroy_();
    185           port = null;
    186         }
    187       });
    188       var rv = requestEvent.dispatch(request, sender, responseCallback);
    189       if (isSendMessage) {
    190         responseCallbackPreserved =
    191             rv && rv.results && $Array.indexOf(rv.results, true) > -1;
    192         if (!responseCallbackPreserved && port) {
    193           // If they didn't access the response callback, they're not
    194           // going to send a response, so clean up the port immediately.
    195           privates(port).impl.destroy_();
    196           port = null;
    197         }
    198       }
    199     }
    200 
    201     privates(port).impl.onDestroy_ = function() {
    202       port.onMessage.removeListener(messageListener);
    203     };
    204     port.onMessage.addListener(messageListener);
    205 
    206     var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
    207     if (isExternal)
    208       eventName += "External";
    209     logActivity.LogEvent(targetExtensionId,
    210                          eventName,
    211                          [sourceExtensionId, sourceUrl]);
    212     return true;
    213   }
    214 
    215   // Called by native code when a channel has been opened to this context.
    216   function dispatchOnConnect(portId,
    217                              channelName,
    218                              sourceTab,
    219                              sourceExtensionId,
    220                              targetExtensionId,
    221                              sourceUrl,
    222                              tlsChannelId) {
    223     // Only create a new Port if someone is actually listening for a connection.
    224     // In addition to being an optimization, this also fixes a bug where if 2
    225     // channels were opened to and from the same process, closing one would
    226     // close both.
    227     var extensionId = processNatives.GetExtensionId();
    228 
    229     // messaging_bindings.cc should ensure that this method only gets called for
    230     // the right extension.
    231     logging.CHECK(targetExtensionId == extensionId);
    232 
    233     if (ports[getOppositePortId(portId)])
    234       return false;  // this channel was opened by us, so ignore it
    235 
    236     // Determine whether this is coming from another extension, so we can use
    237     // the right event.
    238     var isExternal = sourceExtensionId != extensionId;
    239 
    240     var sender = {};
    241     if (sourceExtensionId != '')
    242       sender.id = sourceExtensionId;
    243     if (sourceUrl)
    244       sender.url = sourceUrl;
    245     if (sourceTab)
    246       sender.tab = sourceTab;
    247     if (tlsChannelId !== undefined)
    248       sender.tlsChannelId = tlsChannelId;
    249 
    250     // Special case for sendRequest/onRequest and sendMessage/onMessage.
    251     if (channelName == kRequestChannel || channelName == kMessageChannel) {
    252       return dispatchOnRequest(portId, channelName, sender,
    253                                sourceExtensionId, targetExtensionId, sourceUrl,
    254                                isExternal);
    255     }
    256 
    257     var connectEvent = null;
    258     if (chrome.runtime) {
    259       connectEvent = isExternal ? chrome.runtime.onConnectExternal
    260                                 : chrome.runtime.onConnect;
    261     }
    262     if (!connectEvent)
    263       return false;
    264     if (!connectEvent.hasListeners())
    265       return false;
    266 
    267     var port = createPort(portId, channelName);
    268     port.sender = sender;
    269     if (processNatives.manifestVersion < 2)
    270       port.tab = port.sender.tab;
    271 
    272     var eventName = (isExternal ?
    273         "runtime.onConnectExternal" : "runtime.onConnect");
    274     connectEvent.dispatch(port);
    275     logActivity.LogEvent(targetExtensionId,
    276                          eventName,
    277                          [sourceExtensionId]);
    278     return true;
    279   };
    280 
    281   // Called by native code when a channel has been closed.
    282   function dispatchOnDisconnect(portId, errorMessage) {
    283     var port = ports[portId];
    284     if (port) {
    285       // Update the renderer's port bookkeeping, without notifying the browser.
    286       messagingNatives.CloseChannel(portId, false);
    287       if (errorMessage)
    288         lastError.set('Port', errorMessage, null, chrome);
    289       try {
    290         port.onDisconnect.dispatch(port);
    291       } finally {
    292         privates(port).impl.destroy_();
    293         lastError.clear(chrome);
    294       }
    295     }
    296   };
    297 
    298   // Called by native code when a message has been sent to the given port.
    299   function dispatchOnMessage(msg, portId) {
    300     var port = ports[portId];
    301     if (port) {
    302       if (msg)
    303         msg = $JSON.parse(msg);
    304       port.onMessage.dispatch(msg, port);
    305     }
    306   };
    307 
    308   // Shared implementation used by tabs.sendMessage and runtime.sendMessage.
    309   function sendMessageImpl(port, request, responseCallback) {
    310     if (port.name != kNativeMessageChannel)
    311       port.postMessage(request);
    312 
    313     if (port.name == kMessageChannel && !responseCallback) {
    314       // TODO(mpcomplete): Do this for the old sendRequest API too, after
    315       // verifying it doesn't break anything.
    316       // Go ahead and disconnect immediately if the sender is not expecting
    317       // a response.
    318       port.disconnect();
    319       return;
    320     }
    321 
    322     // Ensure the callback exists for the older sendRequest API.
    323     if (!responseCallback)
    324       responseCallback = function() {};
    325 
    326     // Note: make sure to manually remove the onMessage/onDisconnect listeners
    327     // that we added before destroying the Port, a workaround to a bug in Port
    328     // where any onMessage/onDisconnect listeners added but not removed will
    329     // be leaked when the Port is destroyed.
    330     // http://crbug.com/320723 tracks a sustainable fix.
    331 
    332     function disconnectListener() {
    333       // For onDisconnects, we only notify the callback if there was an error.
    334       if (chrome.runtime && chrome.runtime.lastError)
    335         responseCallback();
    336     }
    337 
    338     function messageListener(response) {
    339       try {
    340         responseCallback(response);
    341       } finally {
    342         port.disconnect();
    343       }
    344     }
    345 
    346     privates(port).impl.onDestroy_ = function() {
    347       port.onDisconnect.removeListener(disconnectListener);
    348       port.onMessage.removeListener(messageListener);
    349     };
    350     port.onDisconnect.addListener(disconnectListener);
    351     port.onMessage.addListener(messageListener);
    352   };
    353 
    354   function sendMessageUpdateArguments(functionName, hasOptionsArgument) {
    355     // skip functionName and hasOptionsArgument
    356     var args = $Array.slice(arguments, 2);
    357     var alignedArgs = messagingUtils.alignSendMessageArguments(args,
    358         hasOptionsArgument);
    359     if (!alignedArgs)
    360       throw new Error('Invalid arguments to ' + functionName + '.');
    361     return alignedArgs;
    362   }
    363 
    364 var Port = utils.expose('Port', PortImpl, { functions: [
    365     'disconnect',
    366     'postMessage'
    367   ],
    368   properties: [
    369     'name',
    370     'onDisconnect',
    371     'onMessage'
    372   ] });
    373 
    374 exports.kRequestChannel = kRequestChannel;
    375 exports.kMessageChannel = kMessageChannel;
    376 exports.kNativeMessageChannel = kNativeMessageChannel;
    377 exports.Port = Port;
    378 exports.createPort = createPort;
    379 exports.sendMessageImpl = sendMessageImpl;
    380 exports.sendMessageUpdateArguments = sendMessageUpdateArguments;
    381 
    382 // For C++ code to call.
    383 exports.hasPort = hasPort;
    384 exports.dispatchOnConnect = dispatchOnConnect;
    385 exports.dispatchOnDisconnect = dispatchOnDisconnect;
    386 exports.dispatchOnMessage = dispatchOnMessage;
    387