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