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