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 unloadEvent
= require('unload_event');
16 var utils
= require('utils');
17 var messagingUtils
= require('messaging_utils');
19 // The reserved channel name for the sendRequest/send(Native)Message APIs.
20 // Note: sendRequest is deprecated.
21 var kRequestChannel
= "chrome.extension.sendRequest";
22 var kMessageChannel
= "chrome.runtime.sendMessage";
23 var kNativeMessageChannel
= "chrome.runtime.sendNativeMessage";
25 // Map of port IDs to port object.
28 // Map of port IDs to unloadEvent listeners. Keep track of these to free the
29 // unloadEvent listeners when ports are closed.
30 var portReleasers
= {};
32 // Change even to odd and vice versa, to get the other side of a given
34 function getOppositePortId(portId
) { return portId
^ 1; }
36 // Port object. Represents a connection to another script context through
37 // which messages can be passed.
38 function PortImpl(portId
, opt_name
) {
39 this.portId_
= portId
;
42 var portSchema
= {name
: 'port', $ref
: 'runtime.Port'};
43 var options
= {unmanaged
: true};
44 this.onDisconnect
= new Event(null, [portSchema
], options
);
45 this.onMessage
= new Event(
47 [{name
: 'message', type
: 'any', optional
: true}, portSchema
],
49 this.onDestroy_
= null;
52 // Sends a message asynchronously to the context on the other end of this
54 PortImpl
.prototype.postMessage = function(msg
) {
55 // JSON.stringify doesn't support a root object which is undefined.
56 if (msg
=== undefined)
58 msg
= $JSON
.stringify(msg
);
59 if (msg
=== undefined) {
60 // JSON.stringify can fail with unserializable objects. Log an error and
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');
70 messagingNatives
.PostMessage(this.portId_
, msg
);
73 // Disconnects the port from the other end.
74 PortImpl
.prototype.disconnect = function() {
75 messagingNatives
.CloseChannel(this.portId_
, true);
79 PortImpl
.prototype.destroy_ = function() {
80 var portId
= this.portId_
;
84 privates(this.onDisconnect
).impl
.destroy_();
85 privates(this.onMessage
).impl
.destroy_();
87 messagingNatives
.PortRelease(portId
);
88 unloadEvent
.removeListener(portReleasers
[portId
]);
91 delete portReleasers
[portId
];
94 // Returns true if the specified port id is in this context. This is used by
95 // the C++ to avoid creating the javascript message for all the contexts that
96 // don't care about a particular message.
97 function hasPort(portId
) {
98 return portId
in ports
;
101 // Hidden port creation function. We don't want to expose an API that lets
102 // people add arbitrary port IDs to the port list.
103 function createPort(portId
, opt_name
) {
105 throw new Error("Port '" + portId
+ "' already exists.");
106 var port
= new Port(portId
, opt_name
);
107 ports
[portId
] = port
;
108 portReleasers
[portId
] = $Function
.bind(messagingNatives
.PortRelease
,
111 unloadEvent
.addListener(portReleasers
[portId
]);
112 messagingNatives
.PortAddRef(portId
);
116 // Helper function for dispatchOnRequest.
117 function handleSendRequestError(isSendMessage
,
118 responseCallbackPreserved
,
123 var eventName
= isSendMessage
? "runtime.onMessage" : "extension.onRequest";
124 if (isSendMessage
&& !responseCallbackPreserved
) {
125 $Array
.push(errorMsg
,
126 "The chrome." + eventName
+ " listener must return true if you " +
127 "want to send a response after the listener returns");
129 $Array
.push(errorMsg
,
130 "Cannot send a response more than once per chrome." + eventName
+
131 " listener per document");
133 $Array
.push(errorMsg
, "(message was sent by extension" + sourceExtensionId
);
134 if (sourceExtensionId
!= "" && sourceExtensionId
!= targetExtensionId
)
135 $Array
.push(errorMsg
, "for extension " + targetExtensionId
);
137 $Array
.push(errorMsg
, "for URL " + sourceUrl
);
138 lastError
.set(eventName
, errorMsg
.join(" ") + ").", null, chrome
);
141 // Helper function for dispatchOnConnect
142 function dispatchOnRequest(portId
, channelName
, sender
,
143 sourceExtensionId
, targetExtensionId
, sourceUrl
,
145 var isSendMessage
= channelName
== kMessageChannel
;
146 var requestEvent
= null;
148 if (chrome
.runtime
) {
149 requestEvent
= isExternal
? chrome
.runtime
.onMessageExternal
150 : chrome
.runtime
.onMessage
;
153 if (chrome
.extension
) {
154 requestEvent
= isExternal
? chrome
.extension
.onRequestExternal
155 : chrome
.extension
.onRequest
;
160 if (!requestEvent
.hasListeners())
162 var port
= createPort(portId
, channelName
);
164 function messageListener(request
) {
165 var responseCallbackPreserved
= false;
166 var responseCallback = function(response
) {
168 port
.postMessage(response
);
169 privates(port
).impl
.destroy_();
172 // We nulled out port when sending the response, and now the page
173 // is trying to send another response for the same request.
174 handleSendRequestError(isSendMessage
, responseCallbackPreserved
,
175 sourceExtensionId
, targetExtensionId
);
178 // In case the extension never invokes the responseCallback, and also
179 // doesn't keep a reference to it, we need to clean up the port. Do
180 // so by attaching to the garbage collection of the responseCallback
181 // using some native hackery.
183 // If the context is destroyed before this has a chance to execute,
184 // BindToGC knows to release |portId| (important for updating C++ state
185 // both in this renderer and on the other end). We don't need to clear
186 // any JavaScript state, as calling destroy_() would usually do - but
187 // the context has been destroyed, so there isn't any JS state to clear.
188 messagingNatives
.BindToGC(responseCallback
, function() {
190 privates(port
).impl
.destroy_();
194 var rv
= requestEvent
.dispatch(request
, sender
, responseCallback
);
196 responseCallbackPreserved
=
197 rv
&& rv
.results
&& $Array
.indexOf(rv
.results
, true) > -1;
198 if (!responseCallbackPreserved
&& port
) {
199 // If they didn't access the response callback, they're not
200 // going to send a response, so clean up the port immediately.
201 privates(port
).impl
.destroy_();
207 privates(port
).impl
.onDestroy_ = function() {
208 port
.onMessage
.removeListener(messageListener
);
210 port
.onMessage
.addListener(messageListener
);
212 var eventName
= isSendMessage
? "runtime.onMessage" : "extension.onRequest";
214 eventName
+= "External";
215 logActivity
.LogEvent(targetExtensionId
,
217 [sourceExtensionId
, sourceUrl
]);
221 // Called by native code when a channel has been opened to this context.
222 function dispatchOnConnect(portId
,
231 // Only create a new Port if someone is actually listening for a connection.
232 // In addition to being an optimization, this also fixes a bug where if 2
233 // channels were opened to and from the same process, closing one would
235 var extensionId
= processNatives
.GetExtensionId();
237 // messaging_bindings.cc should ensure that this method only gets called for
238 // the right extension.
239 logging
.CHECK(targetExtensionId
== extensionId
);
241 if (ports
[getOppositePortId(portId
)])
242 return false; // this channel was opened by us, so ignore it
244 // Determine whether this is coming from another extension, so we can use
246 var isExternal
= sourceExtensionId
!= extensionId
;
249 if (sourceExtensionId
!= '')
250 sender
.id
= sourceExtensionId
;
252 sender
.url
= sourceUrl
;
254 sender
.tab
= sourceTab
;
255 if (sourceFrameId
>= 0)
256 sender
.frameId
= sourceFrameId
;
257 if (typeof guestProcessId
!= 'undefined') {
258 // Note that |guestProcessId| is not a standard field on MessageSender and
259 // should not be exposed to drive-by extensions; it is only exposed to
260 // component extensions.
261 logging
.CHECK(processNatives
.IsComponentExtension(),
262 "GuestProcessId can only be exposed to component extensions.");
263 sender
.guestProcessId
= guestProcessId
;
265 if (typeof tlsChannelId
!= 'undefined')
266 sender
.tlsChannelId
= tlsChannelId
;
268 // Special case for sendRequest/onRequest and sendMessage/onMessage.
269 if (channelName
== kRequestChannel
|| channelName
== kMessageChannel
) {
270 return dispatchOnRequest(portId
, channelName
, sender
,
271 sourceExtensionId
, targetExtensionId
, sourceUrl
,
275 var connectEvent
= null;
276 if (chrome
.runtime
) {
277 connectEvent
= isExternal
? chrome
.runtime
.onConnectExternal
278 : chrome
.runtime
.onConnect
;
282 if (!connectEvent
.hasListeners())
285 var port
= createPort(portId
, channelName
);
286 port
.sender
= sender
;
287 if (processNatives
.manifestVersion
< 2)
288 port
.tab
= port
.sender
.tab
;
290 var eventName
= (isExternal
?
291 "runtime.onConnectExternal" : "runtime.onConnect");
292 connectEvent
.dispatch(port
);
293 logActivity
.LogEvent(targetExtensionId
,
295 [sourceExtensionId
]);
299 // Called by native code when a channel has been closed.
300 function dispatchOnDisconnect(portId
, errorMessage
) {
301 var port
= ports
[portId
];
303 // Update the renderer's port bookkeeping, without notifying the browser.
304 messagingNatives
.CloseChannel(portId
, false);
306 lastError
.set('Port', errorMessage
, null, chrome
);
308 port
.onDisconnect
.dispatch(port
);
310 privates(port
).impl
.destroy_();
311 lastError
.clear(chrome
);
316 // Called by native code when a message has been sent to the given port.
317 function dispatchOnMessage(msg
, portId
) {
318 var port
= ports
[portId
];
321 msg
= $JSON
.parse(msg
);
322 port
.onMessage
.dispatch(msg
, port
);
326 // Shared implementation used by tabs.sendMessage and runtime.sendMessage.
327 function sendMessageImpl(port
, request
, responseCallback
) {
328 if (port
.name
!= kNativeMessageChannel
)
329 port
.postMessage(request
);
331 if (port
.name
== kMessageChannel
&& !responseCallback
) {
332 // TODO(mpcomplete): Do this for the old sendRequest API too, after
333 // verifying it doesn't break anything.
334 // Go ahead and disconnect immediately if the sender is not expecting
340 // Ensure the callback exists for the older sendRequest API.
341 if (!responseCallback
)
342 responseCallback = function() {};
344 // Note: make sure to manually remove the onMessage/onDisconnect listeners
345 // that we added before destroying the Port, a workaround to a bug in Port
346 // where any onMessage/onDisconnect listeners added but not removed will
347 // be leaked when the Port is destroyed.
348 // http://crbug.com/320723 tracks a sustainable fix.
350 function disconnectListener() {
351 // For onDisconnects, we only notify the callback if there was an error.
352 if (chrome
.runtime
&& chrome
.runtime
.lastError
)
356 function messageListener(response
) {
358 responseCallback(response
);
364 privates(port
).impl
.onDestroy_ = function() {
365 port
.onDisconnect
.removeListener(disconnectListener
);
366 port
.onMessage
.removeListener(messageListener
);
368 port
.onDisconnect
.addListener(disconnectListener
);
369 port
.onMessage
.addListener(messageListener
);
372 function sendMessageUpdateArguments(functionName
, hasOptionsArgument
) {
373 // skip functionName and hasOptionsArgument
374 var args
= $Array
.slice(arguments
, 2);
375 var alignedArgs
= messagingUtils
.alignSendMessageArguments(args
,
378 throw new Error('Invalid arguments to ' + functionName
+ '.');
382 var Port
= utils
.expose('Port', PortImpl
, { functions
: [
392 exports
.kRequestChannel
= kRequestChannel
;
393 exports
.kMessageChannel
= kMessageChannel
;
394 exports
.kNativeMessageChannel
= kNativeMessageChannel
;
396 exports
.createPort
= createPort
;
397 exports
.sendMessageImpl
= sendMessageImpl
;
398 exports
.sendMessageUpdateArguments
= sendMessageUpdateArguments
;
400 // For C++ code to call.
401 exports
.hasPort
= hasPort
;
402 exports
.dispatchOnConnect
= dispatchOnConnect
;
403 exports
.dispatchOnDisconnect
= dispatchOnDisconnect
;
404 exports
.dispatchOnMessage
= dispatchOnMessage
;