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