Reland the ULONG -> SIZE_T change from 317177
[chromium-blink-merge.git] / extensions / renderer / resources / messaging.js
blob7a5c91040305c2ea9a86acc6020276f5b278bc29
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.
5 // chrome.runtime.messaging API implementation.
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');
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";
25   // Map of port IDs to port object.
26   var ports = {};
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 = {};
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; }
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;
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   }
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   };
73   // Disconnects the port from the other end.
74   PortImpl.prototype.disconnect = function() {
75     messagingNatives.CloseChannel(this.portId_, true);
76     this.destroy_();
77   };
79   PortImpl.prototype.destroy_ = function() {
80     var portId = this.portId_;
82     if (this.onDestroy_)
83       this.onDestroy_();
84     privates(this.onDisconnect).impl.destroy_();
85     privates(this.onMessage).impl.destroy_();
87     messagingNatives.PortRelease(portId);
88     unloadEvent.removeListener(portReleasers[portId]);
90     delete ports[portId];
91     delete portReleasers[portId];
92   };
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   };
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   };
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   }
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);
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     }
201     privates(port).impl.onDestroy_ = function() {
202       port.onMessage.removeListener(messageListener);
203     };
204     port.onMessage.addListener(messageListener);
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   }
215   // Called by native code when a channel has been opened to this context.
216   function dispatchOnConnect(portId,
217                              channelName,
218                              sourceTab,
219                              sourceFrameId,
220                              guestProcessId,
221                              sourceExtensionId,
222                              targetExtensionId,
223                              sourceUrl,
224                              tlsChannelId) {
225     // Only create a new Port if someone is actually listening for a connection.
226     // In addition to being an optimization, this also fixes a bug where if 2
227     // channels were opened to and from the same process, closing one would
228     // close both.
229     var extensionId = processNatives.GetExtensionId();
231     // messaging_bindings.cc should ensure that this method only gets called for
232     // the right extension.
233     logging.CHECK(targetExtensionId == extensionId);
235     if (ports[getOppositePortId(portId)])
236       return false;  // this channel was opened by us, so ignore it
238     // Determine whether this is coming from another extension, so we can use
239     // the right event.
240     var isExternal = sourceExtensionId != extensionId;
242     var sender = {};
243     if (sourceExtensionId != '')
244       sender.id = sourceExtensionId;
245     if (sourceUrl)
246       sender.url = sourceUrl;
247     if (sourceTab)
248       sender.tab = sourceTab;
249     if (sourceFrameId >= 0)
250       sender.frameId = sourceFrameId;
251     if (typeof guestProcessId != 'undefined') {
252       // Note that |guestProcessId| is not a standard field on MessageSender and
253       // should not be exposed to drive-by extensions; it is only exposed to
254       // component extensions.
255       logging.CHECK(processNatives.IsComponentExtension(),
256           "GuestProcessId can only be exposed to component extensions.");
257       sender.guestProcessId = guestProcessId;
258     }
259     if (typeof tlsChannelId != 'undefined')
260       sender.tlsChannelId = tlsChannelId;
262     // Special case for sendRequest/onRequest and sendMessage/onMessage.
263     if (channelName == kRequestChannel || channelName == kMessageChannel) {
264       return dispatchOnRequest(portId, channelName, sender,
265                                sourceExtensionId, targetExtensionId, sourceUrl,
266                                isExternal);
267     }
269     var connectEvent = null;
270     if (chrome.runtime) {
271       connectEvent = isExternal ? chrome.runtime.onConnectExternal
272                                 : chrome.runtime.onConnect;
273     }
274     if (!connectEvent)
275       return false;
276     if (!connectEvent.hasListeners())
277       return false;
279     var port = createPort(portId, channelName);
280     port.sender = sender;
281     if (processNatives.manifestVersion < 2)
282       port.tab = port.sender.tab;
284     var eventName = (isExternal ?
285         "runtime.onConnectExternal" : "runtime.onConnect");
286     connectEvent.dispatch(port);
287     logActivity.LogEvent(targetExtensionId,
288                          eventName,
289                          [sourceExtensionId]);
290     return true;
291   };
293   // Called by native code when a channel has been closed.
294   function dispatchOnDisconnect(portId, errorMessage) {
295     var port = ports[portId];
296     if (port) {
297       // Update the renderer's port bookkeeping, without notifying the browser.
298       messagingNatives.CloseChannel(portId, false);
299       if (errorMessage)
300         lastError.set('Port', errorMessage, null, chrome);
301       try {
302         port.onDisconnect.dispatch(port);
303       } finally {
304         privates(port).impl.destroy_();
305         lastError.clear(chrome);
306       }
307     }
308   };
310   // Called by native code when a message has been sent to the given port.
311   function dispatchOnMessage(msg, portId) {
312     var port = ports[portId];
313     if (port) {
314       if (msg)
315         msg = $JSON.parse(msg);
316       port.onMessage.dispatch(msg, port);
317     }
318   };
320   // Shared implementation used by tabs.sendMessage and runtime.sendMessage.
321   function sendMessageImpl(port, request, responseCallback) {
322     if (port.name != kNativeMessageChannel)
323       port.postMessage(request);
325     if (port.name == kMessageChannel && !responseCallback) {
326       // TODO(mpcomplete): Do this for the old sendRequest API too, after
327       // verifying it doesn't break anything.
328       // Go ahead and disconnect immediately if the sender is not expecting
329       // a response.
330       port.disconnect();
331       return;
332     }
334     // Ensure the callback exists for the older sendRequest API.
335     if (!responseCallback)
336       responseCallback = function() {};
338     // Note: make sure to manually remove the onMessage/onDisconnect listeners
339     // that we added before destroying the Port, a workaround to a bug in Port
340     // where any onMessage/onDisconnect listeners added but not removed will
341     // be leaked when the Port is destroyed.
342     // http://crbug.com/320723 tracks a sustainable fix.
344     function disconnectListener() {
345       // For onDisconnects, we only notify the callback if there was an error.
346       if (chrome.runtime && chrome.runtime.lastError)
347         responseCallback();
348     }
350     function messageListener(response) {
351       try {
352         responseCallback(response);
353       } finally {
354         port.disconnect();
355       }
356     }
358     privates(port).impl.onDestroy_ = function() {
359       port.onDisconnect.removeListener(disconnectListener);
360       port.onMessage.removeListener(messageListener);
361     };
362     port.onDisconnect.addListener(disconnectListener);
363     port.onMessage.addListener(messageListener);
364   };
366   function sendMessageUpdateArguments(functionName, hasOptionsArgument) {
367     // skip functionName and hasOptionsArgument
368     var args = $Array.slice(arguments, 2);
369     var alignedArgs = messagingUtils.alignSendMessageArguments(args,
370         hasOptionsArgument);
371     if (!alignedArgs)
372       throw new Error('Invalid arguments to ' + functionName + '.');
373     return alignedArgs;
374   }
376 var Port = utils.expose('Port', PortImpl, { functions: [
377     'disconnect',
378     'postMessage'
379   ],
380   properties: [
381     'name',
382     'onDisconnect',
383     'onMessage'
384   ] });
386 exports.kRequestChannel = kRequestChannel;
387 exports.kMessageChannel = kMessageChannel;
388 exports.kNativeMessageChannel = kNativeMessageChannel;
389 exports.Port = Port;
390 exports.createPort = createPort;
391 exports.sendMessageImpl = sendMessageImpl;
392 exports.sendMessageUpdateArguments = sendMessageUpdateArguments;
394 // For C++ code to call.
395 exports.hasPort = hasPort;
396 exports.dispatchOnConnect = dispatchOnConnect;
397 exports.dispatchOnDisconnect = dispatchOnDisconnect;
398 exports.dispatchOnMessage = dispatchOnMessage;