Supervised user whitelists: Cleanup
[chromium-blink-merge.git] / extensions / renderer / resources / messaging.js
blob9fc29d49752b1e788edc7015cf1c8e4725e5f5e6
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 lastError = require('lastError');
10   var logActivity = requireNative('activityLogger');
11   var logging = requireNative('logging');
12   var messagingNatives = requireNative('messaging_natives');
13   var Port = require('port').Port;
14   var processNatives = requireNative('process');
15   var unloadEvent = require('unload_event');
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   // Map of port IDs to unloadEvent listeners. Keep track of these to free the
28   // unloadEvent listeners when ports are closed.
29   var portReleasers = {};
31   // Change even to odd and vice versa, to get the other side of a given
32   // channel.
33   function getOppositePortId(portId) { return portId ^ 1; }
35   // Returns true if the specified port id is in this context. This is used by
36   // the C++ to avoid creating the javascript message for all the contexts that
37   // don't care about a particular message.
38   function hasPort(portId) {
39     return portId in ports;
40   };
42   // Hidden port creation function.  We don't want to expose an API that lets
43   // people add arbitrary port IDs to the port list.
44   function createPort(portId, opt_name) {
45     if (ports[portId])
46       throw new Error("Port '" + portId + "' already exists.");
47     var port = new Port(portId, opt_name);
48     ports[portId] = port;
49     portReleasers[portId] = $Function.bind(messagingNatives.PortRelease,
50                                            this,
51                                            portId);
52     unloadEvent.addListener(portReleasers[portId]);
53     messagingNatives.PortAddRef(portId);
54     return port;
55   };
57   // Called when a Port is destroyed. Does general accounting cleanup.
58   function onPortDestroyed(port) {
59     var portId = privates(port).impl.portId_;
60     unloadEvent.removeListener(portReleasers[portId]);
61     delete ports[portId];
62     delete portReleasers[portId];
63   }
65   // Helper function for dispatchOnRequest.
66   function handleSendRequestError(isSendMessage,
67                                   responseCallbackPreserved,
68                                   sourceExtensionId,
69                                   targetExtensionId,
70                                   sourceUrl) {
71     var errorMsg = [];
72     var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
73     if (isSendMessage && !responseCallbackPreserved) {
74       $Array.push(errorMsg,
75           "The chrome." + eventName + " listener must return true if you " +
76           "want to send a response after the listener returns");
77     } else {
78       $Array.push(errorMsg,
79           "Cannot send a response more than once per chrome." + eventName +
80           " listener per document");
81     }
82     $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId);
83     if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId)
84       $Array.push(errorMsg, "for extension " + targetExtensionId);
85     if (sourceUrl != "")
86       $Array.push(errorMsg, "for URL " + sourceUrl);
87     lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome);
88   }
90   // Helper function for dispatchOnConnect
91   function dispatchOnRequest(portId, channelName, sender,
92                              sourceExtensionId, targetExtensionId, sourceUrl,
93                              isExternal) {
94     var isSendMessage = channelName == kMessageChannel;
95     var requestEvent = null;
96     if (isSendMessage) {
97       if (chrome.runtime) {
98         requestEvent = isExternal ? chrome.runtime.onMessageExternal
99                                   : chrome.runtime.onMessage;
100       }
101     } else {
102       if (chrome.extension) {
103         requestEvent = isExternal ? chrome.extension.onRequestExternal
104                                   : chrome.extension.onRequest;
105       }
106     }
107     if (!requestEvent)
108       return false;
109     if (!requestEvent.hasListeners())
110       return false;
111     var port = createPort(portId, channelName);
113     function messageListener(request) {
114       var responseCallbackPreserved = false;
115       var responseCallback = function(response) {
116         if (port) {
117           port.postMessage(response);
118           privates(port).impl.destroy_();
119           port = null;
120         } else {
121           // We nulled out port when sending the response, and now the page
122           // is trying to send another response for the same request.
123           handleSendRequestError(isSendMessage, responseCallbackPreserved,
124                                  sourceExtensionId, targetExtensionId);
125         }
126       };
127       // In case the extension never invokes the responseCallback, and also
128       // doesn't keep a reference to it, we need to clean up the port. Do
129       // so by attaching to the garbage collection of the responseCallback
130       // using some native hackery.
131       messagingNatives.BindToGC(responseCallback, function() {
132         if (port) {
133           privates(port).impl.destroy_();
134           port = null;
135         }
136       });
137       var rv = requestEvent.dispatch(request, sender, responseCallback);
138       if (isSendMessage) {
139         responseCallbackPreserved =
140             rv && rv.results && $Array.indexOf(rv.results, true) > -1;
141         if (!responseCallbackPreserved && port) {
142           // If they didn't access the response callback, they're not
143           // going to send a response, so clean up the port immediately.
144           privates(port).impl.destroy_();
145           port = null;
146         }
147       }
148     }
150     privates(port).impl.onDestroy_ = function() {
151       port.onMessage.removeListener(messageListener);
152       onPortDestroyed(port);
153     };
154     port.onMessage.addListener(messageListener);
156     var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
157     if (isExternal)
158       eventName += "External";
159     logActivity.LogEvent(targetExtensionId,
160                          eventName,
161                          [sourceExtensionId, sourceUrl]);
162     return true;
163   }
165   // Called by native code when a channel has been opened to this context.
166   function dispatchOnConnect(portId,
167                              channelName,
168                              sourceTab,
169                              sourceFrameId,
170                              guestProcessId,
171                              sourceExtensionId,
172                              targetExtensionId,
173                              sourceUrl,
174                              tlsChannelId) {
175     // Only create a new Port if someone is actually listening for a connection.
176     // In addition to being an optimization, this also fixes a bug where if 2
177     // channels were opened to and from the same process, closing one would
178     // close both.
179     var extensionId = processNatives.GetExtensionId();
181     // messaging_bindings.cc should ensure that this method only gets called for
182     // the right extension.
183     logging.CHECK(targetExtensionId == extensionId);
185     if (ports[getOppositePortId(portId)])
186       return false;  // this channel was opened by us, so ignore it
188     // Determine whether this is coming from another extension, so we can use
189     // the right event.
190     var isExternal = sourceExtensionId != extensionId;
192     var sender = {};
193     if (sourceExtensionId != '')
194       sender.id = sourceExtensionId;
195     if (sourceUrl)
196       sender.url = sourceUrl;
197     if (sourceTab)
198       sender.tab = sourceTab;
199     if (sourceFrameId >= 0)
200       sender.frameId = sourceFrameId;
201     if (typeof guestProcessId != 'undefined') {
202       // Note that |guestProcessId| is not a standard field on MessageSender and
203       // should not be exposed to drive-by extensions; it is only exposed to
204       // component extensions.
205       logging.CHECK(processNatives.IsComponentExtension(),
206           "GuestProcessId can only be exposed to component extensions.");
207       sender.guestProcessId = guestProcessId;
208     }
209     if (typeof tlsChannelId != 'undefined')
210       sender.tlsChannelId = tlsChannelId;
212     // Special case for sendRequest/onRequest and sendMessage/onMessage.
213     if (channelName == kRequestChannel || channelName == kMessageChannel) {
214       return dispatchOnRequest(portId, channelName, sender,
215                                sourceExtensionId, targetExtensionId, sourceUrl,
216                                isExternal);
217     }
219     var connectEvent = null;
220     if (chrome.runtime) {
221       connectEvent = isExternal ? chrome.runtime.onConnectExternal
222                                 : chrome.runtime.onConnect;
223     }
224     if (!connectEvent)
225       return false;
226     if (!connectEvent.hasListeners())
227       return false;
229     var port = createPort(portId, channelName);
230     port.sender = sender;
231     if (processNatives.manifestVersion < 2)
232       port.tab = port.sender.tab;
234     var eventName = (isExternal ?
235         "runtime.onConnectExternal" : "runtime.onConnect");
236     connectEvent.dispatch(port);
237     logActivity.LogEvent(targetExtensionId,
238                          eventName,
239                          [sourceExtensionId]);
240     return true;
241   };
243   // Called by native code when a channel has been closed.
244   function dispatchOnDisconnect(portId, errorMessage) {
245     var port = ports[portId];
246     if (port) {
247       // Update the renderer's port bookkeeping, without notifying the browser.
248       messagingNatives.CloseChannel(portId, false);
249       if (errorMessage)
250         lastError.set('Port', errorMessage, null, chrome);
251       try {
252         port.onDisconnect.dispatch(port);
253       } finally {
254         privates(port).impl.destroy_();
255         lastError.clear(chrome);
256       }
257     }
258   };
260   // Called by native code when a message has been sent to the given port.
261   function dispatchOnMessage(msg, portId) {
262     var port = ports[portId];
263     if (port) {
264       if (msg)
265         msg = $JSON.parse(msg);
266       port.onMessage.dispatch(msg, port);
267     }
268   };
270   // Shared implementation used by tabs.sendMessage and runtime.sendMessage.
271   function sendMessageImpl(port, request, responseCallback) {
272     if (port.name != kNativeMessageChannel)
273       port.postMessage(request);
275     if (port.name == kMessageChannel && !responseCallback) {
276       // TODO(mpcomplete): Do this for the old sendRequest API too, after
277       // verifying it doesn't break anything.
278       // Go ahead and disconnect immediately if the sender is not expecting
279       // a response.
280       port.disconnect();
281       return;
282     }
284     // Ensure the callback exists for the older sendRequest API.
285     if (!responseCallback)
286       responseCallback = function() {};
288     // Note: make sure to manually remove the onMessage/onDisconnect listeners
289     // that we added before destroying the Port, a workaround to a bug in Port
290     // where any onMessage/onDisconnect listeners added but not removed will
291     // be leaked when the Port is destroyed.
292     // http://crbug.com/320723 tracks a sustainable fix.
294     function disconnectListener() {
295       // For onDisconnects, we only notify the callback if there was an error.
296       if (chrome.runtime && chrome.runtime.lastError)
297         responseCallback();
298     }
300     function messageListener(response) {
301       try {
302         responseCallback(response);
303       } finally {
304         port.disconnect();
305       }
306     }
308     privates(port).impl.onDestroy_ = function() {
309       port.onDisconnect.removeListener(disconnectListener);
310       port.onMessage.removeListener(messageListener);
311       onPortDestroyed(port);
312     };
313     port.onDisconnect.addListener(disconnectListener);
314     port.onMessage.addListener(messageListener);
315   };
317   function sendMessageUpdateArguments(functionName, hasOptionsArgument) {
318     // skip functionName and hasOptionsArgument
319     var args = $Array.slice(arguments, 2);
320     var alignedArgs = messagingUtils.alignSendMessageArguments(args,
321         hasOptionsArgument);
322     if (!alignedArgs)
323       throw new Error('Invalid arguments to ' + functionName + '.');
324     return alignedArgs;
325   }
327 exports.kRequestChannel = kRequestChannel;
328 exports.kMessageChannel = kMessageChannel;
329 exports.kNativeMessageChannel = kNativeMessageChannel;
330 exports.createPort = createPort;
331 exports.sendMessageImpl = sendMessageImpl;
332 exports.sendMessageUpdateArguments = sendMessageUpdateArguments;
334 // For C++ code to call.
335 exports.hasPort = hasPort;
336 exports.dispatchOnConnect = dispatchOnConnect;
337 exports.dispatchOnDisconnect = dispatchOnDisconnect;
338 exports.dispatchOnMessage = dispatchOnMessage;