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