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