Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / messaging.js
bloba39ecb1cb6c6b7f7c13ac907fc58397cfc3fd445
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.
27 var ports = {};
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
34 // channel.
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;
41 this.name = opt_name;
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(
47 null,
48 [{name: 'message', type: 'any', optional: true}, portSchema],
49 options);
50 this.onDestroy_ = null;
53 // Sends a message asynchronously to the context on the other end of this
54 // port.
55 Port.prototype.postMessage = function(msg) {
56 // JSON.stringify doesn't support a root object which is undefined.
57 if (msg === undefined)
58 msg = null;
59 msg = $JSON.stringify(msg);
60 if (msg === undefined) {
61 // JSON.stringify can fail with unserializable objects. Log an error and
62 // drop the message.
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');
69 return;
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);
77 this.destroy_();
80 Port.prototype.destroy_ = function() {
81 var portId = this.portId_;
83 if (this.onDestroy_)
84 this.onDestroy_();
85 this.onDisconnect.destroy_();
86 this.onMessage.destroy_();
88 messagingNatives.PortRelease(portId);
89 unloadEvent.removeListener(portReleasers[portId]);
91 delete ports[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) {
105 if (ports[portId])
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,
110 this,
111 portId);
112 unloadEvent.addListener(portReleasers[portId]);
113 messagingNatives.PortAddRef(portId);
114 return port;
117 // Helper function for dispatchOnRequest.
118 function handleSendRequestError(isSendMessage,
119 responseCallbackPreserved,
120 sourceExtensionId,
121 targetExtensionId,
122 sourceUrl) {
123 var errorMsg = [];
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");
129 } else {
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);
137 if (sourceUrl != "")
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,
145 isExternal) {
146 var isSendMessage = channelName == kMessageChannel;
147 var requestEvent = null;
148 if (isSendMessage) {
149 if (chrome.runtime) {
150 requestEvent = isExternal ? chrome.runtime.onMessageExternal
151 : chrome.runtime.onMessage;
153 } else {
154 if (chrome.extension) {
155 requestEvent = isExternal ? chrome.extension.onRequestExternal
156 : chrome.extension.onRequest;
159 if (!requestEvent)
160 return false;
161 if (!requestEvent.hasListeners())
162 return false;
163 var port = createPort(portId, channelName);
165 function messageListener(request) {
166 var responseCallbackPreserved = false;
167 var responseCallback = function(response) {
168 if (port) {
169 port.postMessage(response);
170 port.destroy_();
171 port = null;
172 } else {
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() {
184 if (port) {
185 port.destroy_();
186 port = null;
189 if (!isSendMessage) {
190 requestEvent.dispatch(request, sender, responseCallback);
191 } else {
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.
198 port.destroy_();
199 port = null;
204 port.onDestroy_ = function() {
205 port.onMessage.removeListener(messageListener);
207 port.onMessage.addListener(messageListener);
209 var eventName = (isSendMessage ?
210 (isExternal ?
211 "runtime.onMessageExternal" : "runtime.onMessage") :
212 (isExternal ?
213 "extension.onRequestExternal" : "extension.onRequest"));
214 logActivity.LogEvent(targetExtensionId,
215 eventName,
216 [sourceExtensionId, sourceUrl]);
217 return true;
220 // Called by native code when a channel has been opened to this context.
221 function dispatchOnConnect(portId,
222 channelName,
223 sourceTab,
224 sourceExtensionId,
225 targetExtensionId,
226 sourceUrl,
227 tlsChannelId) {
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
231 // close both.
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
240 // the right event.
241 var isExternal = sourceExtensionId != extensionId;
243 var sender = {};
244 if (sourceExtensionId != '')
245 sender.id = sourceExtensionId;
246 if (sourceUrl)
247 sender.url = sourceUrl;
248 if (sourceTab)
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,
257 isExternal);
260 var connectEvent = null;
261 if (chrome.runtime) {
262 connectEvent = isExternal ? chrome.runtime.onConnectExternal
263 : chrome.runtime.onConnect;
265 if (!connectEvent)
266 return false;
267 if (!connectEvent.hasListeners())
268 return false;
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,
279 eventName,
280 [sourceExtensionId]);
281 return true;
284 // Called by native code when a channel has been closed.
285 function dispatchOnDisconnect(portId, errorMessage) {
286 var port = ports[portId];
287 if (port) {
288 // Update the renderer's port bookkeeping, without notifying the browser.
289 messagingNatives.CloseChannel(portId, false);
290 if (errorMessage)
291 lastError.set('Port', errorMessage, null, chrome);
292 try {
293 port.onDisconnect.dispatch(port);
294 } finally {
295 port.destroy_();
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];
304 if (port) {
305 if (msg)
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
320 // a response.
321 port.disconnect();
322 return;
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)
338 responseCallback();
341 function messageListener(response) {
342 try {
343 responseCallback(response);
344 } finally {
345 port.disconnect();
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,
361 hasOptionsArgument);
362 if (!alignedArgs)
363 throw new Error('Invalid arguments to ' + functionName + '.');
364 return alignedArgs;
367 exports.kRequestChannel = kRequestChannel;
368 exports.kMessageChannel = kMessageChannel;
369 exports.kNativeMessageChannel = kNativeMessageChannel;
370 exports.Port = Port;
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;