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