• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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// 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 logging = requireNative('logging');
16  var messagingNatives = requireNative('messaging_natives');
17  var processNatives = requireNative('process');
18  var unloadEvent = require('unload_event');
19  var utils = require('utils');
20  var messagingUtils = require('messaging_utils');
21
22  // The reserved channel name for the sendRequest/send(Native)Message APIs.
23  // Note: sendRequest is deprecated.
24  var kRequestChannel = "chrome.extension.sendRequest";
25  var kMessageChannel = "chrome.runtime.sendMessage";
26  var kNativeMessageChannel = "chrome.runtime.sendNativeMessage";
27
28  // Map of port IDs to port object.
29  var ports = {};
30
31  // Map of port IDs to unloadEvent listeners. Keep track of these to free the
32  // unloadEvent listeners when ports are closed.
33  var portReleasers = {};
34
35  // Change even to odd and vice versa, to get the other side of a given
36  // channel.
37  function getOppositePortId(portId) { return portId ^ 1; }
38
39  // Port object.  Represents a connection to another script context through
40  // which messages can be passed.
41  function PortImpl(portId, opt_name) {
42    this.portId_ = portId;
43    this.name = opt_name;
44
45    var portSchema = {name: 'port', $ref: 'runtime.Port'};
46    var options = {unmanaged: true};
47    this.onDisconnect = new Event(null, [portSchema], options);
48    this.onMessage = new Event(
49        null,
50        [{name: 'message', type: 'any', optional: true}, portSchema],
51        options);
52    this.onDestroy_ = null;
53  }
54
55  // Sends a message asynchronously to the context on the other end of this
56  // port.
57  PortImpl.prototype.postMessage = function(msg) {
58    // JSON.stringify doesn't support a root object which is undefined.
59    if (msg === undefined)
60      msg = null;
61    msg = $JSON.stringify(msg);
62    if (msg === undefined) {
63      // JSON.stringify can fail with unserializable objects. Log an error and
64      // drop the message.
65      //
66      // TODO(kalman/mpcomplete): it would be better to do the same validation
67      // here that we do for runtime.sendMessage (and variants), i.e. throw an
68      // schema validation Error, but just maintain the old behaviour until
69      // there's a good reason not to (http://crbug.com/263077).
70      console.error('Illegal argument to Port.postMessage');
71      return;
72    }
73    messagingNatives.PostMessage(this.portId_, msg);
74  };
75
76  // Disconnects the port from the other end.
77  PortImpl.prototype.disconnect = function() {
78    messagingNatives.CloseChannel(this.portId_, true);
79    this.destroy_();
80  };
81
82  PortImpl.prototype.destroy_ = function() {
83    var portId = this.portId_;
84
85    if (this.onDestroy_)
86      this.onDestroy_();
87    privates(this.onDisconnect).impl.destroy_();
88    privates(this.onMessage).impl.destroy_();
89
90    messagingNatives.PortRelease(portId);
91    unloadEvent.removeListener(portReleasers[portId]);
92
93    delete ports[portId];
94    delete portReleasers[portId];
95  };
96
97  // Returns true if the specified port id is in this context. This is used by
98  // the C++ to avoid creating the javascript message for all the contexts that
99  // don't care about a particular message.
100  function hasPort(portId) {
101    return portId in ports;
102  };
103
104  // Hidden port creation function.  We don't want to expose an API that lets
105  // people add arbitrary port IDs to the port list.
106  function createPort(portId, opt_name) {
107    if (ports[portId])
108      throw new Error("Port '" + portId + "' already exists.");
109    var port = new Port(portId, opt_name);
110    ports[portId] = port;
111    portReleasers[portId] = $Function.bind(messagingNatives.PortRelease,
112                                           this,
113                                           portId);
114    unloadEvent.addListener(portReleasers[portId]);
115    messagingNatives.PortAddRef(portId);
116    return port;
117  };
118
119  // Helper function for dispatchOnRequest.
120  function handleSendRequestError(isSendMessage,
121                                  responseCallbackPreserved,
122                                  sourceExtensionId,
123                                  targetExtensionId,
124                                  sourceUrl) {
125    var errorMsg = [];
126    var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
127    if (isSendMessage && !responseCallbackPreserved) {
128      $Array.push(errorMsg,
129          "The chrome." + eventName + " listener must return true if you " +
130          "want to send a response after the listener returns");
131    } else {
132      $Array.push(errorMsg,
133          "Cannot send a response more than once per chrome." + eventName +
134          " listener per document");
135    }
136    $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId);
137    if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId)
138      $Array.push(errorMsg, "for extension " + targetExtensionId);
139    if (sourceUrl != "")
140      $Array.push(errorMsg, "for URL " + sourceUrl);
141    lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome);
142  }
143
144  // Helper function for dispatchOnConnect
145  function dispatchOnRequest(portId, channelName, sender,
146                             sourceExtensionId, targetExtensionId, sourceUrl,
147                             isExternal) {
148    var isSendMessage = channelName == kMessageChannel;
149    var requestEvent = null;
150    if (isSendMessage) {
151      if (chrome.runtime) {
152        requestEvent = isExternal ? chrome.runtime.onMessageExternal
153                                  : chrome.runtime.onMessage;
154      }
155    } else {
156      if (chrome.extension) {
157        requestEvent = isExternal ? chrome.extension.onRequestExternal
158                                  : chrome.extension.onRequest;
159      }
160    }
161    if (!requestEvent)
162      return false;
163    if (!requestEvent.hasListeners())
164      return false;
165    var port = createPort(portId, channelName);
166
167    function messageListener(request) {
168      var responseCallbackPreserved = false;
169      var responseCallback = function(response) {
170        if (port) {
171          port.postMessage(response);
172          privates(port).impl.destroy_();
173          port = null;
174        } else {
175          // We nulled out port when sending the response, and now the page
176          // is trying to send another response for the same request.
177          handleSendRequestError(isSendMessage, responseCallbackPreserved,
178                                 sourceExtensionId, targetExtensionId);
179        }
180      };
181      // In case the extension never invokes the responseCallback, and also
182      // doesn't keep a reference to it, we need to clean up the port. Do
183      // so by attaching to the garbage collection of the responseCallback
184      // using some native hackery.
185      messagingNatives.BindToGC(responseCallback, function() {
186        if (port) {
187          privates(port).impl.destroy_();
188          port = null;
189        }
190      });
191      var rv = requestEvent.dispatch(request, sender, responseCallback);
192      if (isSendMessage) {
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          privates(port).impl.destroy_();
199          port = null;
200        }
201      }
202    }
203
204    privates(port).impl.onDestroy_ = function() {
205      port.onMessage.removeListener(messageListener);
206    };
207    port.onMessage.addListener(messageListener);
208
209    var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
210    if (isExternal)
211      eventName += "External";
212    logActivity.LogEvent(targetExtensionId,
213                         eventName,
214                         [sourceExtensionId, sourceUrl]);
215    return true;
216  }
217
218  // Called by native code when a channel has been opened to this context.
219  function dispatchOnConnect(portId,
220                             channelName,
221                             sourceTab,
222                             sourceExtensionId,
223                             targetExtensionId,
224                             sourceUrl,
225                             tlsChannelId) {
226    // Only create a new Port if someone is actually listening for a connection.
227    // In addition to being an optimization, this also fixes a bug where if 2
228    // channels were opened to and from the same process, closing one would
229    // close both.
230    var extensionId = processNatives.GetExtensionId();
231
232    // messaging_bindings.cc should ensure that this method only gets called for
233    // the right extension.
234    logging.CHECK(targetExtensionId == extensionId);
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        privates(port).impl.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    privates(port).impl.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
367var Port = utils.expose('Port', PortImpl, { functions: [
368    'disconnect',
369    'postMessage'
370  ],
371  properties: [
372    'name',
373    'onDisconnect',
374    'onMessage'
375  ] });
376
377exports.kRequestChannel = kRequestChannel;
378exports.kMessageChannel = kMessageChannel;
379exports.kNativeMessageChannel = kNativeMessageChannel;
380exports.Port = Port;
381exports.createPort = createPort;
382exports.sendMessageImpl = sendMessageImpl;
383exports.sendMessageUpdateArguments = sendMessageUpdateArguments;
384
385// For C++ code to call.
386exports.hasPort = hasPort;
387exports.dispatchOnConnect = dispatchOnConnect;
388exports.dispatchOnDisconnect = dispatchOnDisconnect;
389exports.dispatchOnMessage = dispatchOnMessage;
390