• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
367exports.kRequestChannel = kRequestChannel;
368exports.kMessageChannel = kMessageChannel;
369exports.kNativeMessageChannel = kNativeMessageChannel;
370exports.Port = Port;
371exports.createPort = createPort;
372exports.sendMessageImpl = sendMessageImpl;
373exports.sendMessageUpdateArguments = sendMessageUpdateArguments;
374
375// For C++ code to call.
376exports.hasPort = hasPort;
377exports.dispatchOnConnect = dispatchOnConnect;
378exports.dispatchOnDisconnect = dispatchOnDisconnect;
379exports.dispatchOnMessage = dispatchOnMessage;
380