| OLD | NEW |
| (Empty) |
| 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 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 |