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.
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
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;
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) {
46 throw new Error("Port '" + portId + "' already exists.");
47 var port = new Port(portId, opt_name);
49 portReleasers[portId] = $Function.bind(messagingNatives.PortRelease,
52 unloadEvent.addListener(portReleasers[portId]);
53 messagingNatives.PortAddRef(portId);
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]);
62 delete portReleasers[portId];
65 // Helper function for dispatchOnRequest.
66 function handleSendRequestError(isSendMessage,
67 responseCallbackPreserved,
72 var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
73 if (isSendMessage && !responseCallbackPreserved) {
75 "The chrome." + eventName + " listener must return true if you " +
76 "want to send a response after the listener returns");
79 "Cannot send a response more than once per chrome." + eventName +
80 " listener per document");
82 $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId);
83 if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId)
84 $Array.push(errorMsg, "for extension " + targetExtensionId);
86 $Array.push(errorMsg, "for URL " + sourceUrl);
87 lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome);
90 // Helper function for dispatchOnConnect
91 function dispatchOnRequest(portId, channelName, sender,
92 sourceExtensionId, targetExtensionId, sourceUrl,
94 var isSendMessage = channelName == kMessageChannel;
95 var requestEvent = null;
98 requestEvent = isExternal ? chrome.runtime.onMessageExternal
99 : chrome.runtime.onMessage;
102 if (chrome.extension) {
103 requestEvent = isExternal ? chrome.extension.onRequestExternal
104 : chrome.extension.onRequest;
109 if (!requestEvent.hasListeners())
111 var port = createPort(portId, channelName);
113 function messageListener(request) {
114 var responseCallbackPreserved = false;
115 var responseCallback = function(response) {
117 port.postMessage(response);
118 privates(port).impl.destroy_();
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);
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() {
133 privates(port).impl.destroy_();
137 var rv = requestEvent.dispatch(request, sender, responseCallback);
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_();
150 privates(port).impl.onDestroy_ = function() {
151 port.onMessage.removeListener(messageListener);
152 onPortDestroyed(port);
154 port.onMessage.addListener(messageListener);
156 var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
158 eventName += "External";
159 logActivity.LogEvent(targetExtensionId,
161 [sourceExtensionId, sourceUrl]);
165 // Called by native code when a channel has been opened to this context.
166 function dispatchOnConnect(portId,
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
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
190 var isExternal = sourceExtensionId != extensionId;
193 if (sourceExtensionId != '')
194 sender.id = sourceExtensionId;
196 sender.url = sourceUrl;
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;
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,
219 var connectEvent = null;
220 if (chrome.runtime) {
221 connectEvent = isExternal ? chrome.runtime.onConnectExternal
222 : chrome.runtime.onConnect;
226 if (!connectEvent.hasListeners())
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,
239 [sourceExtensionId]);
243 // Called by native code when a channel has been closed.
244 function dispatchOnDisconnect(portId, errorMessage) {
245 var port = ports[portId];
247 // Update the renderer's port bookkeeping, without notifying the browser.
248 messagingNatives.CloseChannel(portId, false);
250 lastError.set('Port', errorMessage, null, chrome);
252 port.onDisconnect.dispatch(port);
254 privates(port).impl.destroy_();
255 lastError.clear(chrome);
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];
265 msg = $JSON.parse(msg);
266 port.onMessage.dispatch(msg, port);
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
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)
300 function messageListener(response) {
302 responseCallback(response);
308 privates(port).impl.onDestroy_ = function() {
309 port.onDisconnect.removeListener(disconnectListener);
310 port.onMessage.removeListener(messageListener);
311 onPortDestroyed(port);
313 port.onDisconnect.addListener(disconnectListener);
314 port.onMessage.addListener(messageListener);
317 function sendMessageUpdateArguments(functionName, hasOptionsArgument) {
318 // skip functionName and hasOptionsArgument
319 var args = $Array.slice(arguments, 2);
320 var alignedArgs = messagingUtils.alignSendMessageArguments(args,
323 throw new Error('Invalid arguments to ' + functionName + '.');
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;