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. 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 logging = requireNative('logging'); 16 var messagingNatives = requireNative('messaging_natives'); 17 var processNatives = requireNative('process'); 18 var unloadEvent = require('unload_event'); 19 var utils = require('utils'); 20 var messagingUtils = require('messaging_utils'); 21 22 // The reserved channel name for the sendRequest/send(Native)Message APIs. 23 // Note: sendRequest is deprecated. 24 var kRequestChannel = "chrome.extension.sendRequest"; 25 var kMessageChannel = "chrome.runtime.sendMessage"; 26 var kNativeMessageChannel = "chrome.runtime.sendNativeMessage"; 27 28 // Map of port IDs to port object. 29 var ports = {}; 30 31 // Map of port IDs to unloadEvent listeners. Keep track of these to free the 32 // unloadEvent listeners when ports are closed. 33 var portReleasers = {}; 34 35 // Change even to odd and vice versa, to get the other side of a given 36 // channel. 37 function getOppositePortId(portId) { return portId ^ 1; } 38 39 // Port object. Represents a connection to another script context through 40 // which messages can be passed. 41 function PortImpl(portId, opt_name) { 42 this.portId_ = portId; 43 this.name = opt_name; 44 45 var portSchema = {name: 'port', $ref: 'runtime.Port'}; 46 var options = {unmanaged: true}; 47 this.onDisconnect = new Event(null, [portSchema], options); 48 this.onMessage = new Event( 49 null, 50 [{name: 'message', type: 'any', optional: true}, portSchema], 51 options); 52 this.onDestroy_ = null; 53 } 54 55 // Sends a message asynchronously to the context on the other end of this 56 // port. 57 PortImpl.prototype.postMessage = function(msg) { 58 // JSON.stringify doesn't support a root object which is undefined. 59 if (msg === undefined) 60 msg = null; 61 msg = $JSON.stringify(msg); 62 if (msg === undefined) { 63 // JSON.stringify can fail with unserializable objects. Log an error and 64 // drop the message. 65 // 66 // TODO(kalman/mpcomplete): it would be better to do the same validation 67 // here that we do for runtime.sendMessage (and variants), i.e. throw an 68 // schema validation Error, but just maintain the old behaviour until 69 // there's a good reason not to (http://crbug.com/263077). 70 console.error('Illegal argument to Port.postMessage'); 71 return; 72 } 73 messagingNatives.PostMessage(this.portId_, msg); 74 }; 75 76 // Disconnects the port from the other end. 77 PortImpl.prototype.disconnect = function() { 78 messagingNatives.CloseChannel(this.portId_, true); 79 this.destroy_(); 80 }; 81 82 PortImpl.prototype.destroy_ = function() { 83 var portId = this.portId_; 84 85 if (this.onDestroy_) 86 this.onDestroy_(); 87 privates(this.onDisconnect).impl.destroy_(); 88 privates(this.onMessage).impl.destroy_(); 89 90 messagingNatives.PortRelease(portId); 91 unloadEvent.removeListener(portReleasers[portId]); 92 93 delete ports[portId]; 94 delete portReleasers[portId]; 95 }; 96 97 // Returns true if the specified port id is in this context. This is used by 98 // the C++ to avoid creating the javascript message for all the contexts that 99 // don't care about a particular message. 100 function hasPort(portId) { 101 return portId in ports; 102 }; 103 104 // Hidden port creation function. We don't want to expose an API that lets 105 // people add arbitrary port IDs to the port list. 106 function createPort(portId, opt_name) { 107 if (ports[portId]) 108 throw new Error("Port '" + portId + "' already exists."); 109 var port = new Port(portId, opt_name); 110 ports[portId] = port; 111 portReleasers[portId] = $Function.bind(messagingNatives.PortRelease, 112 this, 113 portId); 114 unloadEvent.addListener(portReleasers[portId]); 115 messagingNatives.PortAddRef(portId); 116 return port; 117 }; 118 119 // Helper function for dispatchOnRequest. 120 function handleSendRequestError(isSendMessage, 121 responseCallbackPreserved, 122 sourceExtensionId, 123 targetExtensionId, 124 sourceUrl) { 125 var errorMsg = []; 126 var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest"; 127 if (isSendMessage && !responseCallbackPreserved) { 128 $Array.push(errorMsg, 129 "The chrome." + eventName + " listener must return true if you " + 130 "want to send a response after the listener returns"); 131 } else { 132 $Array.push(errorMsg, 133 "Cannot send a response more than once per chrome." + eventName + 134 " listener per document"); 135 } 136 $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId); 137 if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId) 138 $Array.push(errorMsg, "for extension " + targetExtensionId); 139 if (sourceUrl != "") 140 $Array.push(errorMsg, "for URL " + sourceUrl); 141 lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome); 142 } 143 144 // Helper function for dispatchOnConnect 145 function dispatchOnRequest(portId, channelName, sender, 146 sourceExtensionId, targetExtensionId, sourceUrl, 147 isExternal) { 148 var isSendMessage = channelName == kMessageChannel; 149 var requestEvent = null; 150 if (isSendMessage) { 151 if (chrome.runtime) { 152 requestEvent = isExternal ? chrome.runtime.onMessageExternal 153 : chrome.runtime.onMessage; 154 } 155 } else { 156 if (chrome.extension) { 157 requestEvent = isExternal ? chrome.extension.onRequestExternal 158 : chrome.extension.onRequest; 159 } 160 } 161 if (!requestEvent) 162 return false; 163 if (!requestEvent.hasListeners()) 164 return false; 165 var port = createPort(portId, channelName); 166 167 function messageListener(request) { 168 var responseCallbackPreserved = false; 169 var responseCallback = function(response) { 170 if (port) { 171 port.postMessage(response); 172 privates(port).impl.destroy_(); 173 port = null; 174 } else { 175 // We nulled out port when sending the response, and now the page 176 // is trying to send another response for the same request. 177 handleSendRequestError(isSendMessage, responseCallbackPreserved, 178 sourceExtensionId, targetExtensionId); 179 } 180 }; 181 // In case the extension never invokes the responseCallback, and also 182 // doesn't keep a reference to it, we need to clean up the port. Do 183 // so by attaching to the garbage collection of the responseCallback 184 // using some native hackery. 185 messagingNatives.BindToGC(responseCallback, function() { 186 if (port) { 187 privates(port).impl.destroy_(); 188 port = null; 189 } 190 }); 191 var rv = requestEvent.dispatch(request, sender, responseCallback); 192 if (isSendMessage) { 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 privates(port).impl.destroy_(); 199 port = null; 200 } 201 } 202 } 203 204 privates(port).impl.onDestroy_ = function() { 205 port.onMessage.removeListener(messageListener); 206 }; 207 port.onMessage.addListener(messageListener); 208 209 var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest"; 210 if (isExternal) 211 eventName += "External"; 212 logActivity.LogEvent(targetExtensionId, 213 eventName, 214 [sourceExtensionId, sourceUrl]); 215 return true; 216 } 217 218 // Called by native code when a channel has been opened to this context. 219 function dispatchOnConnect(portId, 220 channelName, 221 sourceTab, 222 sourceExtensionId, 223 targetExtensionId, 224 sourceUrl, 225 tlsChannelId) { 226 // Only create a new Port if someone is actually listening for a connection. 227 // In addition to being an optimization, this also fixes a bug where if 2 228 // channels were opened to and from the same process, closing one would 229 // close both. 230 var extensionId = processNatives.GetExtensionId(); 231 232 // messaging_bindings.cc should ensure that this method only gets called for 233 // the right extension. 234 logging.CHECK(targetExtensionId == extensionId); 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 privates(port).impl.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 privates(port).impl.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 367var Port = utils.expose('Port', PortImpl, { functions: [ 368 'disconnect', 369 'postMessage' 370 ], 371 properties: [ 372 'name', 373 'onDisconnect', 374 'onMessage' 375 ] }); 376 377exports.kRequestChannel = kRequestChannel; 378exports.kMessageChannel = kMessageChannel; 379exports.kNativeMessageChannel = kNativeMessageChannel; 380exports.Port = Port; 381exports.createPort = createPort; 382exports.sendMessageImpl = sendMessageImpl; 383exports.sendMessageUpdateArguments = sendMessageUpdateArguments; 384 385// For C++ code to call. 386exports.hasPort = hasPort; 387exports.dispatchOnConnect = dispatchOnConnect; 388exports.dispatchOnDisconnect = dispatchOnDisconnect; 389exports.dispatchOnMessage = dispatchOnMessage; 390