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.
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.
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');
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";
26 // Map of port IDs to port object.
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
= {};
33 // Change even to odd and vice versa, to get the other side of a given
35 function getOppositePortId(portId
) { return portId
^ 1; }
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
;
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(
48 [{name
: 'message', type
: 'any', optional
: true}, portSchema
],
50 this.onDestroy_
= null;
53 // Sends a message asynchronously to the context on the other end of this
55 Port
.prototype.postMessage = function(msg
) {
56 // JSON.stringify doesn't support a root object which is undefined.
57 if (msg
=== undefined)
59 msg
= $JSON
.stringify(msg
);
60 if (msg
=== undefined) {
61 // JSON.stringify can fail with unserializable objects. Log an error and
64 // TODO(kalman/mpcomplete): it would be better to do the same validation
65 // here that we do for runtime.sendMessage (and variants), i.e. throw an
66 // schema validation Error, but just maintain the old behaviour until
67 // there's a good reason not to (http://crbug.com/263077).
68 console
.error('Illegal argument to Port.postMessage');
71 messagingNatives
.PostMessage(this.portId_
, msg
);
74 // Disconnects the port from the other end.
75 Port
.prototype.disconnect = function() {
76 messagingNatives
.CloseChannel(this.portId_
, true);
80 Port
.prototype.destroy_ = function() {
81 var portId
= this.portId_
;
85 this.onDisconnect
.destroy_();
86 this.onMessage
.destroy_();
88 messagingNatives
.PortRelease(portId
);
89 unloadEvent
.removeListener(portReleasers
[portId
]);
92 delete portReleasers
[portId
];
95 // Returns true if the specified port id is in this context. This is used by
96 // the C++ to avoid creating the javascript message for all the contexts that
97 // don't care about a particular message.
98 function hasPort(portId
) {
99 return portId
in ports
;
102 // Hidden port creation function. We don't want to expose an API that lets
103 // people add arbitrary port IDs to the port list.
104 function createPort(portId
, opt_name
) {
106 throw new Error("Port '" + portId
+ "' already exists.");
107 var port
= new Port(portId
, opt_name
);
108 ports
[portId
] = port
;
109 portReleasers
[portId
] = $Function
.bind(messagingNatives
.PortRelease
,
112 unloadEvent
.addListener(portReleasers
[portId
]);
113 messagingNatives
.PortAddRef(portId
);
117 // Helper function for dispatchOnRequest.
118 function handleSendRequestError(isSendMessage
,
119 responseCallbackPreserved
,
124 var eventName
= isSendMessage
? "runtime.onMessage" : "extension.onRequest";
125 if (isSendMessage
&& !responseCallbackPreserved
) {
126 $Array
.push(errorMsg
,
127 "The chrome." + eventName
+ " listener must return true if you " +
128 "want to send a response after the listener returns");
130 $Array
.push(errorMsg
,
131 "Cannot send a response more than once per chrome." + eventName
+
132 " listener per document");
134 $Array
.push(errorMsg
, "(message was sent by extension" + sourceExtensionId
);
135 if (sourceExtensionId
!= "" && sourceExtensionId
!= targetExtensionId
)
136 $Array
.push(errorMsg
, "for extension " + targetExtensionId
);
138 $Array
.push(errorMsg
, "for URL " + sourceUrl
);
139 lastError
.set(eventName
, errorMsg
.join(" ") + ").", null, chrome
);
142 // Helper function for dispatchOnConnect
143 function dispatchOnRequest(portId
, channelName
, sender
,
144 sourceExtensionId
, targetExtensionId
, sourceUrl
,
146 var isSendMessage
= channelName
== kMessageChannel
;
147 var requestEvent
= null;
149 if (chrome
.runtime
) {
150 requestEvent
= isExternal
? chrome
.runtime
.onMessageExternal
151 : chrome
.runtime
.onMessage
;
154 if (chrome
.extension
) {
155 requestEvent
= isExternal
? chrome
.extension
.onRequestExternal
156 : chrome
.extension
.onRequest
;
161 if (!requestEvent
.hasListeners())
163 var port
= createPort(portId
, channelName
);
165 function messageListener(request
) {
166 var responseCallbackPreserved
= false;
167 var responseCallback = function(response
) {
169 port
.postMessage(response
);
173 // We nulled out port when sending the response, and now the page
174 // is trying to send another response for the same request.
175 handleSendRequestError(isSendMessage
, responseCallbackPreserved
,
176 sourceExtensionId
, targetExtensionId
);
179 // In case the extension never invokes the responseCallback, and also
180 // doesn't keep a reference to it, we need to clean up the port. Do
181 // so by attaching to the garbage collection of the responseCallback
182 // using some native hackery.
183 messagingNatives
.BindToGC(responseCallback
, function() {
189 if (!isSendMessage
) {
190 requestEvent
.dispatch(request
, sender
, responseCallback
);
192 var rv
= requestEvent
.dispatch(request
, sender
, responseCallback
);
193 responseCallbackPreserved
=
194 rv
&& rv
.results
&& $Array
.indexOf(rv
.results
, true) > -1;
195 if (!responseCallbackPreserved
&& port
) {
196 // If they didn't access the response callback, they're not
197 // going to send a response, so clean up the port immediately.
204 port
.onDestroy_ = function() {
205 port
.onMessage
.removeListener(messageListener
);
207 port
.onMessage
.addListener(messageListener
);
209 var eventName
= (isSendMessage
?
211 "runtime.onMessageExternal" : "runtime.onMessage") :
213 "extension.onRequestExternal" : "extension.onRequest"));
214 logActivity
.LogEvent(targetExtensionId
,
216 [sourceExtensionId
, sourceUrl
]);
220 // Called by native code when a channel has been opened to this context.
221 function dispatchOnConnect(portId
,
228 // Only create a new Port if someone is actually listening for a connection.
229 // In addition to being an optimization, this also fixes a bug where if 2
230 // channels were opened to and from the same process, closing one would
232 var extensionId
= processNatives
.GetExtensionId();
233 if (targetExtensionId
!= extensionId
)
234 return false; // not for us
236 if (ports
[getOppositePortId(portId
)])
237 return false; // this channel was opened by us, so ignore it
239 // Determine whether this is coming from another extension, so we can use
241 var isExternal
= sourceExtensionId
!= extensionId
;
244 if (sourceExtensionId
!= '')
245 sender
.id
= sourceExtensionId
;
247 sender
.url
= sourceUrl
;
249 sender
.tab
= sourceTab
;
250 if (tlsChannelId
!== undefined)
251 sender
.tlsChannelId
= tlsChannelId
;
253 // Special case for sendRequest/onRequest and sendMessage/onMessage.
254 if (channelName
== kRequestChannel
|| channelName
== kMessageChannel
) {
255 return dispatchOnRequest(portId
, channelName
, sender
,
256 sourceExtensionId
, targetExtensionId
, sourceUrl
,
260 var connectEvent
= null;
261 if (chrome
.runtime
) {
262 connectEvent
= isExternal
? chrome
.runtime
.onConnectExternal
263 : chrome
.runtime
.onConnect
;
267 if (!connectEvent
.hasListeners())
270 var port
= createPort(portId
, channelName
);
271 port
.sender
= sender
;
272 if (processNatives
.manifestVersion
< 2)
273 port
.tab
= port
.sender
.tab
;
275 var eventName
= (isExternal
?
276 "runtime.onConnectExternal" : "runtime.onConnect");
277 connectEvent
.dispatch(port
);
278 logActivity
.LogEvent(targetExtensionId
,
280 [sourceExtensionId
]);
284 // Called by native code when a channel has been closed.
285 function dispatchOnDisconnect(portId
, errorMessage
) {
286 var port
= ports
[portId
];
288 // Update the renderer's port bookkeeping, without notifying the browser.
289 messagingNatives
.CloseChannel(portId
, false);
291 lastError
.set('Port', errorMessage
, null, chrome
);
293 port
.onDisconnect
.dispatch(port
);
296 lastError
.clear(chrome
);
301 // Called by native code when a message has been sent to the given port.
302 function dispatchOnMessage(msg
, portId
) {
303 var port
= ports
[portId
];
306 msg
= $JSON
.parse(msg
);
307 port
.onMessage
.dispatch(msg
, port
);
311 // Shared implementation used by tabs.sendMessage and runtime.sendMessage.
312 function sendMessageImpl(port
, request
, responseCallback
) {
313 if (port
.name
!= kNativeMessageChannel
)
314 port
.postMessage(request
);
316 if (port
.name
== kMessageChannel
&& !responseCallback
) {
317 // TODO(mpcomplete): Do this for the old sendRequest API too, after
318 // verifying it doesn't break anything.
319 // Go ahead and disconnect immediately if the sender is not expecting
325 // Ensure the callback exists for the older sendRequest API.
326 if (!responseCallback
)
327 responseCallback = function() {};
329 // Note: make sure to manually remove the onMessage/onDisconnect listeners
330 // that we added before destroying the Port, a workaround to a bug in Port
331 // where any onMessage/onDisconnect listeners added but not removed will
332 // be leaked when the Port is destroyed.
333 // http://crbug.com/320723 tracks a sustainable fix.
335 function disconnectListener() {
336 // For onDisconnects, we only notify the callback if there was an error.
337 if (chrome
.runtime
&& chrome
.runtime
.lastError
)
341 function messageListener(response
) {
343 responseCallback(response
);
349 port
.onDestroy_ = function() {
350 port
.onDisconnect
.removeListener(disconnectListener
);
351 port
.onMessage
.removeListener(messageListener
);
353 port
.onDisconnect
.addListener(disconnectListener
);
354 port
.onMessage
.addListener(messageListener
);
357 function sendMessageUpdateArguments(functionName
, hasOptionsArgument
) {
358 // skip functionName and hasOptionsArgument
359 var args
= $Array
.slice(arguments
, 2);
360 var alignedArgs
= messagingUtils
.alignSendMessageArguments(args
,
363 throw new Error('Invalid arguments to ' + functionName
+ '.');
367 exports
.kRequestChannel
= kRequestChannel
;
368 exports
.kMessageChannel
= kMessageChannel
;
369 exports
.kNativeMessageChannel
= kNativeMessageChannel
;
371 exports
.createPort
= createPort
;
372 exports
.sendMessageImpl
= sendMessageImpl
;
373 exports
.sendMessageUpdateArguments
= sendMessageUpdateArguments
;
375 // For C++ code to call.
376 exports
.hasPort
= hasPort
;
377 exports
.dispatchOnConnect
= dispatchOnConnect
;
378 exports
.dispatchOnDisconnect
= dispatchOnDisconnect
;
379 exports
.dispatchOnMessage
= dispatchOnMessage
;