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