[MacViews] Show comboboxes with a native NSMenu
[chromium-blink-merge.git] / extensions / renderer / resources / messaging.js
blob5fe9920588a2315b8087d3af7d5e404ff8995b48
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 utils = require('utils');
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 // Change even to odd and vice versa, to get the other side of a given
28 // channel.
29 function getOppositePortId(portId) { return portId ^ 1; }
31 // Port object. Represents a connection to another script context through
32 // which messages can be passed.
33 function PortImpl(portId, opt_name) {
34 this.portId_ = portId;
35 this.name = opt_name;
37 var portSchema = {name: 'port', $ref: 'runtime.Port'};
38 var options = {unmanaged: true};
39 this.onDisconnect = new Event(null, [portSchema], options);
40 this.onMessage = new Event(
41 null,
42 [{name: 'message', type: 'any', optional: true}, portSchema],
43 options);
44 this.onDestroy_ = null;
47 // Sends a message asynchronously to the context on the other end of this
48 // port.
49 PortImpl.prototype.postMessage = function(msg) {
50 // JSON.stringify doesn't support a root object which is undefined.
51 if (msg === undefined)
52 msg = null;
53 msg = $JSON.stringify(msg);
54 if (msg === undefined) {
55 // JSON.stringify can fail with unserializable objects. Log an error and
56 // drop the message.
58 // TODO(kalman/mpcomplete): it would be better to do the same validation
59 // here that we do for runtime.sendMessage (and variants), i.e. throw an
60 // schema validation Error, but just maintain the old behaviour until
61 // there's a good reason not to (http://crbug.com/263077).
62 console.error('Illegal argument to Port.postMessage');
63 return;
65 messagingNatives.PostMessage(this.portId_, msg);
68 // Disconnects the port from the other end.
69 PortImpl.prototype.disconnect = function() {
70 messagingNatives.CloseChannel(this.portId_, true);
71 this.destroy_();
74 PortImpl.prototype.destroy_ = function() {
75 if (this.onDestroy_)
76 this.onDestroy_();
77 privates(this.onDisconnect).impl.destroy_();
78 privates(this.onMessage).impl.destroy_();
79 messagingNatives.PortRelease(this.portId_);
80 delete ports[this.portId_];
83 // Returns true if the specified port id is in this context. This is used by
84 // the C++ to avoid creating the javascript message for all the contexts that
85 // don't care about a particular message.
86 function hasPort(portId) {
87 return portId in ports;
90 // Hidden port creation function. We don't want to expose an API that lets
91 // people add arbitrary port IDs to the port list.
92 function createPort(portId, opt_name) {
93 if (ports[portId])
94 throw new Error("Port '" + portId + "' already exists.");
95 var port = new Port(portId, opt_name);
96 ports[portId] = port;
97 messagingNatives.PortAddRef(portId);
98 return port;
101 // Helper function for dispatchOnRequest.
102 function handleSendRequestError(isSendMessage,
103 responseCallbackPreserved,
104 sourceExtensionId,
105 targetExtensionId,
106 sourceUrl) {
107 var errorMsg = [];
108 var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
109 if (isSendMessage && !responseCallbackPreserved) {
110 $Array.push(errorMsg,
111 "The chrome." + eventName + " listener must return true if you " +
112 "want to send a response after the listener returns");
113 } else {
114 $Array.push(errorMsg,
115 "Cannot send a response more than once per chrome." + eventName +
116 " listener per document");
118 $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId);
119 if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId)
120 $Array.push(errorMsg, "for extension " + targetExtensionId);
121 if (sourceUrl != "")
122 $Array.push(errorMsg, "for URL " + sourceUrl);
123 lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome);
126 // Helper function for dispatchOnConnect
127 function dispatchOnRequest(portId, channelName, sender,
128 sourceExtensionId, targetExtensionId, sourceUrl,
129 isExternal) {
130 var isSendMessage = channelName == kMessageChannel;
131 var requestEvent = null;
132 if (isSendMessage) {
133 if (chrome.runtime) {
134 requestEvent = isExternal ? chrome.runtime.onMessageExternal
135 : chrome.runtime.onMessage;
137 } else {
138 if (chrome.extension) {
139 requestEvent = isExternal ? chrome.extension.onRequestExternal
140 : chrome.extension.onRequest;
143 if (!requestEvent)
144 return false;
145 if (!requestEvent.hasListeners())
146 return false;
147 var port = createPort(portId, channelName);
149 function messageListener(request) {
150 var responseCallbackPreserved = false;
151 var responseCallback = function(response) {
152 if (port) {
153 port.postMessage(response);
154 privates(port).impl.destroy_();
155 port = null;
156 } else {
157 // We nulled out port when sending the response, and now the page
158 // is trying to send another response for the same request.
159 handleSendRequestError(isSendMessage, responseCallbackPreserved,
160 sourceExtensionId, targetExtensionId);
163 // In case the extension never invokes the responseCallback, and also
164 // doesn't keep a reference to it, we need to clean up the port. Do
165 // so by attaching to the garbage collection of the responseCallback
166 // using some native hackery.
168 // If the context is destroyed before this has a chance to execute,
169 // BindToGC knows to release |portId| (important for updating C++ state
170 // both in this renderer and on the other end). We don't need to clear
171 // any JavaScript state, as calling destroy_() would usually do - but
172 // the context has been destroyed, so there isn't any JS state to clear.
173 messagingNatives.BindToGC(responseCallback, function() {
174 if (port) {
175 privates(port).impl.destroy_();
176 port = null;
178 }, portId);
179 var rv = requestEvent.dispatch(request, sender, responseCallback);
180 if (isSendMessage) {
181 responseCallbackPreserved =
182 rv && rv.results && $Array.indexOf(rv.results, true) > -1;
183 if (!responseCallbackPreserved && port) {
184 // If they didn't access the response callback, they're not
185 // going to send a response, so clean up the port immediately.
186 privates(port).impl.destroy_();
187 port = null;
192 privates(port).impl.onDestroy_ = function() {
193 port.onMessage.removeListener(messageListener);
195 port.onMessage.addListener(messageListener);
197 var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
198 if (isExternal)
199 eventName += "External";
200 logActivity.LogEvent(targetExtensionId,
201 eventName,
202 [sourceExtensionId, sourceUrl]);
203 return true;
206 // Called by native code when a channel has been opened to this context.
207 function dispatchOnConnect(portId,
208 channelName,
209 sourceTab,
210 sourceFrameId,
211 guestProcessId,
212 guestRenderFrameRoutingId,
213 sourceExtensionId,
214 targetExtensionId,
215 sourceUrl,
216 tlsChannelId) {
217 // Only create a new Port if someone is actually listening for a connection.
218 // In addition to being an optimization, this also fixes a bug where if 2
219 // channels were opened to and from the same process, closing one would
220 // close both.
221 var extensionId = processNatives.GetExtensionId();
223 // messaging_bindings.cc should ensure that this method only gets called for
224 // the right extension.
225 logging.CHECK(targetExtensionId == extensionId);
227 if (ports[getOppositePortId(portId)])
228 return false; // this channel was opened by us, so ignore it
230 // Determine whether this is coming from another extension, so we can use
231 // the right event.
232 var isExternal = sourceExtensionId != extensionId;
234 var sender = {};
235 if (sourceExtensionId != '')
236 sender.id = sourceExtensionId;
237 if (sourceUrl)
238 sender.url = sourceUrl;
239 if (sourceTab)
240 sender.tab = sourceTab;
241 if (sourceFrameId >= 0)
242 sender.frameId = sourceFrameId;
243 if (typeof guestProcessId !== 'undefined' &&
244 typeof guestRenderFrameRoutingId !== 'undefined') {
245 // Note that |guestProcessId| and |guestRenderFrameRoutingId| are not
246 // standard fields on MessageSender and should not be exposed to drive-by
247 // extensions; it is only exposed to component extensions.
248 logging.CHECK(processNatives.IsComponentExtension(),
249 "GuestProcessId can only be exposed to component extensions.");
250 sender.guestProcessId = guestProcessId;
251 sender.guestRenderFrameRoutingId = guestRenderFrameRoutingId;
253 if (typeof tlsChannelId != 'undefined')
254 sender.tlsChannelId = tlsChannelId;
256 // Special case for sendRequest/onRequest and sendMessage/onMessage.
257 if (channelName == kRequestChannel || channelName == kMessageChannel) {
258 return dispatchOnRequest(portId, channelName, sender,
259 sourceExtensionId, targetExtensionId, sourceUrl,
260 isExternal);
263 var connectEvent = null;
264 if (chrome.runtime) {
265 connectEvent = isExternal ? chrome.runtime.onConnectExternal
266 : chrome.runtime.onConnect;
268 if (!connectEvent)
269 return false;
270 if (!connectEvent.hasListeners())
271 return false;
273 var port = createPort(portId, channelName);
274 port.sender = sender;
275 if (processNatives.manifestVersion < 2)
276 port.tab = port.sender.tab;
278 var eventName = (isExternal ?
279 "runtime.onConnectExternal" : "runtime.onConnect");
280 connectEvent.dispatch(port);
281 logActivity.LogEvent(targetExtensionId,
282 eventName,
283 [sourceExtensionId]);
284 return true;
287 // Called by native code when a channel has been closed.
288 function dispatchOnDisconnect(portId, errorMessage) {
289 var port = ports[portId];
290 if (port) {
291 // Update the renderer's port bookkeeping, without notifying the browser.
292 messagingNatives.CloseChannel(portId, false);
293 if (errorMessage)
294 lastError.set('Port', errorMessage, null, chrome);
295 try {
296 port.onDisconnect.dispatch(port);
297 } finally {
298 privates(port).impl.destroy_();
299 lastError.clear(chrome);
304 // Called by native code when a message has been sent to the given port.
305 function dispatchOnMessage(msg, portId) {
306 var port = ports[portId];
307 if (port) {
308 if (msg)
309 msg = $JSON.parse(msg);
310 port.onMessage.dispatch(msg, port);
314 // Shared implementation used by tabs.sendMessage and runtime.sendMessage.
315 function sendMessageImpl(port, request, responseCallback) {
316 if (port.name != kNativeMessageChannel)
317 port.postMessage(request);
319 if (port.name == kMessageChannel && !responseCallback) {
320 // TODO(mpcomplete): Do this for the old sendRequest API too, after
321 // verifying it doesn't break anything.
322 // Go ahead and disconnect immediately if the sender is not expecting
323 // a response.
324 port.disconnect();
325 return;
328 // Ensure the callback exists for the older sendRequest API.
329 if (!responseCallback)
330 responseCallback = function() {};
332 // Note: make sure to manually remove the onMessage/onDisconnect listeners
333 // that we added before destroying the Port, a workaround to a bug in Port
334 // where any onMessage/onDisconnect listeners added but not removed will
335 // be leaked when the Port is destroyed.
336 // http://crbug.com/320723 tracks a sustainable fix.
338 function disconnectListener() {
339 // For onDisconnects, we only notify the callback if there was an error.
340 if (chrome.runtime && chrome.runtime.lastError)
341 responseCallback();
344 function messageListener(response) {
345 try {
346 responseCallback(response);
347 } finally {
348 port.disconnect();
352 privates(port).impl.onDestroy_ = function() {
353 port.onDisconnect.removeListener(disconnectListener);
354 port.onMessage.removeListener(messageListener);
356 port.onDisconnect.addListener(disconnectListener);
357 port.onMessage.addListener(messageListener);
360 function sendMessageUpdateArguments(functionName, hasOptionsArgument) {
361 // skip functionName and hasOptionsArgument
362 var args = $Array.slice(arguments, 2);
363 var alignedArgs = messagingUtils.alignSendMessageArguments(args,
364 hasOptionsArgument);
365 if (!alignedArgs)
366 throw new Error('Invalid arguments to ' + functionName + '.');
367 return alignedArgs;
370 var Port = utils.expose('Port', PortImpl, { functions: [
371 'disconnect',
372 'postMessage'
374 properties: [
375 'name',
376 'onDisconnect',
377 'onMessage'
378 ] });
380 exports.kRequestChannel = kRequestChannel;
381 exports.kMessageChannel = kMessageChannel;
382 exports.kNativeMessageChannel = kNativeMessageChannel;
383 exports.Port = Port;
384 exports.createPort = createPort;
385 exports.sendMessageImpl = sendMessageImpl;
386 exports.sendMessageUpdateArguments = sendMessageUpdateArguments;
388 // For C++ code to call.
389 exports.hasPort = hasPort;
390 exports.dispatchOnConnect = dispatchOnConnect;
391 exports.dispatchOnDisconnect = dispatchOnDisconnect;
392 exports.dispatchOnMessage = dispatchOnMessage;