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 messagingUtils = require('messaging_utils'); 19 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"; 25 26 // Map of port IDs to port object. 27 var ports = {}; 28 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 = {}; 32 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; } 36 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; 42 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; 51 } 52 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. 63 // 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; 70 } 71 messagingNatives.PostMessage(this.portId_, msg); 72 }; 73 74 // Disconnects the port from the other end. 75 Port.prototype.disconnect = function() { 76 messagingNatives.CloseChannel(this.portId_, true); 77 this.destroy_(); 78 }; 79 80 Port.prototype.destroy_ = function() { 81 var portId = this.portId_; 82 83 if (this.onDestroy_) 84 this.onDestroy_(); 85 this.onDisconnect.destroy_(); 86 this.onMessage.destroy_(); 87 88 messagingNatives.PortRelease(portId); 89 unloadEvent.removeListener(portReleasers[portId]); 90 91 delete ports[portId]; 92 delete portReleasers[portId]; 93 }; 94 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; 100 }; 101 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; 115 }; 116 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"); 133 } 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); 140 } 141 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; 152 } 153 } else { 154 if (chrome.extension) { 155 requestEvent = isExternal ? chrome.extension.onRequestExternal 156 : chrome.extension.onRequest; 157 } 158 } 159 if (!requestEvent) 160 return false; 161 if (!requestEvent.hasListeners()) 162 return false; 163 var port = createPort(portId, channelName); 164 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); 177 } 178 }; 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; 187 } 188 }); 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; 200 } 201 } 202 } 203 204 port.onDestroy_ = function() { 205 port.onMessage.removeListener(messageListener); 206 }; 207 port.onMessage.addListener(messageListener); 208 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; 218 } 219 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 235 236 if (ports[getOppositePortId(portId)]) 237 return false; // this channel was opened by us, so ignore it 238 239 // Determine whether this is coming from another extension, so we can use 240 // the right event. 241 var isExternal = sourceExtensionId != extensionId; 242 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; 252 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); 258 } 259 260 var connectEvent = null; 261 if (chrome.runtime) { 262 connectEvent = isExternal ? chrome.runtime.onConnectExternal 263 : chrome.runtime.onConnect; 264 } 265 if (!connectEvent) 266 return false; 267 if (!connectEvent.hasListeners()) 268 return false; 269 270 var port = createPort(portId, channelName); 271 port.sender = sender; 272 if (processNatives.manifestVersion < 2) 273 port.tab = port.sender.tab; 274 275 var eventName = (isExternal ? 276 "runtime.onConnectExternal" : "runtime.onConnect"); 277 connectEvent.dispatch(port); 278 logActivity.LogEvent(targetExtensionId, 279 eventName, 280 [sourceExtensionId]); 281 return true; 282 }; 283 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); 297 } 298 } 299 }; 300 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); 308 } 309 }; 310 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); 315 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; 323 } 324 325 // Ensure the callback exists for the older sendRequest API. 326 if (!responseCallback) 327 responseCallback = function() {}; 328 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. 334 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(); 339 } 340 341 function messageListener(response) { 342 try { 343 responseCallback(response); 344 } finally { 345 port.disconnect(); 346 } 347 } 348 349 port.onDestroy_ = function() { 350 port.onDisconnect.removeListener(disconnectListener); 351 port.onMessage.removeListener(messageListener); 352 }; 353 port.onDisconnect.addListener(disconnectListener); 354 port.onMessage.addListener(messageListener); 355 }; 356 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; 365 } 366 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; 374 375 // For C++ code to call. 376 exports.hasPort = hasPort; 377 exports.dispatchOnConnect = dispatchOnConnect; 378 exports.dispatchOnDisconnect = dispatchOnDisconnect; 379 exports.dispatchOnMessage = dispatchOnMessage; 380