OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 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 script contains unprivileged javascript APIs related to chrome | |
6 // extensions. It is loaded by any extension-related context, such as content | |
7 // scripts or background pages. | |
8 // See user_script_slave.cc for script that is loaded by content scripts only. | |
9 // TODO(mpcomplete): we also load this in regular web pages, but don't need to. | |
10 | |
11 var chrome = chrome || {}; | |
12 (function () { | |
13 native function OpenChannelToExtension(sourceId, targetId, name); | |
14 native function CloseChannel(portId, notifyBrowser); | |
15 native function PortAddRef(portId); | |
16 native function PortRelease(portId); | |
17 native function PostMessage(portId, msg); | |
18 native function GetChromeHidden(); | |
19 native function GetL10nMessage(); | |
20 native function Print(); | |
21 | |
22 var chromeHidden = GetChromeHidden(); | |
23 | |
24 // The reserved channel name for the sendRequest API. | |
25 chromeHidden.kRequestChannel = "chrome.extension.sendRequest"; | |
26 | |
27 // Map of port IDs to port object. | |
28 var ports = {}; | |
29 | |
30 // Map of port IDs to chromeHidden.onUnload listeners. Keep track of these | |
31 // to free the onUnload 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 chrome.Port = function(portId, opt_name) { | |
41 this.portId_ = portId; | |
42 this.name = opt_name; | |
43 this.onDisconnect = new chrome.Event(); | |
44 this.onMessage = new chrome.Event(); | |
45 }; | |
46 | |
47 chromeHidden.Port = {}; | |
48 | |
49 // Returns true if the specified port id is in this context. This is used by | |
50 // the C++ to avoid creating the javascript message for all the contexts that | |
51 // don't care about a particular message. | |
52 chromeHidden.Port.hasPort = function(portId) { | |
53 return portId in ports; | |
54 }; | |
55 | |
56 // Hidden port creation function. We don't want to expose an API that lets | |
57 // people add arbitrary port IDs to the port list. | |
58 chromeHidden.Port.createPort = function(portId, opt_name) { | |
59 if (ports[portId]) { | |
60 throw new Error("Port '" + portId + "' already exists."); | |
61 } | |
62 var port = new chrome.Port(portId, opt_name); | |
63 ports[portId] = port; | |
64 portReleasers[portId] = PortRelease.bind(this, portId); | |
65 chromeHidden.onUnload.addListener(portReleasers[portId]); | |
66 | |
67 PortAddRef(portId); | |
68 return port; | |
69 }; | |
70 | |
71 // Called by native code when a channel has been opened to this context. | |
72 chromeHidden.Port.dispatchOnConnect = function(portId, channelName, tab, | |
73 sourceExtensionId, | |
74 targetExtensionId) { | |
75 // Only create a new Port if someone is actually listening for a connection. | |
76 // In addition to being an optimization, this also fixes a bug where if 2 | |
77 // channels were opened to and from the same process, closing one would | |
78 // close both. | |
79 if (targetExtensionId != chromeHidden.extensionId) | |
80 return; // not for us | |
81 if (ports[getOppositePortId(portId)]) | |
82 return; // this channel was opened by us, so ignore it | |
83 | |
84 // Determine whether this is coming from another extension, so we can use | |
85 // the right event. | |
86 var isExternal = sourceExtensionId != chromeHidden.extensionId; | |
87 | |
88 if (tab) | |
89 tab = chromeHidden.JSON.parse(tab); | |
90 var sender = {tab: tab, id: sourceExtensionId}; | |
91 | |
92 // Special case for sendRequest/onRequest. | |
93 if (channelName == chromeHidden.kRequestChannel) { | |
94 var requestEvent = (isExternal ? | |
95 chrome.extension.onRequestExternal : chrome.extension.onRequest); | |
96 if (requestEvent.hasListeners()) { | |
97 var port = chromeHidden.Port.createPort(portId, channelName); | |
98 port.onMessage.addListener(function(request) { | |
99 requestEvent.dispatch(request, sender, function(response) { | |
100 if (port) { | |
101 port.postMessage(response); | |
102 port = null; | |
103 } else { | |
104 // We nulled out port when sending the response, and now the page | |
105 // is trying to send another response for the same request. | |
106 var errorMsg = | |
107 "Cannot send a response more than once per " + | |
108 "chrome.extension.onRequest listener per document (message " + | |
109 "was sent by extension " + sourceExtensionId; | |
110 if (sourceExtensionId != targetExtensionId) { | |
111 errorMsg += " for extension " + targetExtensionId; | |
112 } | |
113 errorMsg += ")."; | |
114 chrome.extension.lastError = {"message": errorMsg}; | |
115 console.error("Could not send response: " + errorMsg); | |
116 } | |
117 }); | |
118 }); | |
119 } | |
120 return; | |
121 } | |
122 | |
123 var connectEvent = (isExternal ? | |
124 chrome.extension.onConnectExternal : chrome.extension.onConnect); | |
125 if (connectEvent.hasListeners()) { | |
126 var port = chromeHidden.Port.createPort(portId, channelName); | |
127 port.sender = sender; | |
128 // TODO(EXTENSIONS_DEPRECATED): port.tab is obsolete. | |
129 port.tab = port.sender.tab; | |
130 | |
131 connectEvent.dispatch(port); | |
132 } | |
133 }; | |
134 | |
135 // Called by native code when a channel has been closed. | |
136 chromeHidden.Port.dispatchOnDisconnect = function( | |
137 portId, connectionInvalid) { | |
138 var port = ports[portId]; | |
139 if (port) { | |
140 // Update the renderer's port bookkeeping, without notifying the browser. | |
141 CloseChannel(portId, false); | |
142 if (connectionInvalid) { | |
143 var errorMsg = | |
144 "Could not establish connection. Receiving end does not exist."; | |
145 chrome.extension.lastError = {"message": errorMsg}; | |
146 console.error("Port error: " + errorMsg); | |
147 } | |
148 try { | |
149 port.onDisconnect.dispatch(port); | |
150 } finally { | |
151 port.destroy_(); | |
152 delete chrome.extension.lastError; | |
153 } | |
154 } | |
155 }; | |
156 | |
157 // Called by native code when a message has been sent to the given port. | |
158 chromeHidden.Port.dispatchOnMessage = function(msg, portId) { | |
159 var port = ports[portId]; | |
160 if (port) { | |
161 if (msg) { | |
162 msg = chromeHidden.JSON.parse(msg); | |
163 } | |
164 port.onMessage.dispatch(msg, port); | |
165 } | |
166 }; | |
167 | |
168 // Sends a message asynchronously to the context on the other end of this | |
169 // port. | |
170 chrome.Port.prototype.postMessage = function(msg) { | |
171 // JSON.stringify doesn't support a root object which is undefined. | |
172 if (msg === undefined) | |
173 msg = null; | |
174 PostMessage(this.portId_, chromeHidden.JSON.stringify(msg)); | |
175 }; | |
176 | |
177 // Disconnects the port from the other end. | |
178 chrome.Port.prototype.disconnect = function() { | |
179 CloseChannel(this.portId_, true); | |
180 this.destroy_(); | |
181 }; | |
182 | |
183 chrome.Port.prototype.destroy_ = function() { | |
184 var portId = this.portId_; | |
185 | |
186 this.onDisconnect.destroy_(); | |
187 this.onMessage.destroy_(); | |
188 | |
189 PortRelease(portId); | |
190 chromeHidden.onUnload.removeListener(portReleasers[portId]); | |
191 | |
192 delete ports[portId]; | |
193 delete portReleasers[portId]; | |
194 }; | |
195 | |
196 // This function is called on context initialization for both content scripts | |
197 // and extension contexts. | |
198 chromeHidden.onLoad.addListener(function(extensionId, isExtensionProcess, | |
199 inIncognitoContext) { | |
200 chromeHidden.extensionId = extensionId; | |
201 | |
202 chrome.extension = chrome.extension || {}; | |
203 chrome.self = chrome.extension; | |
204 | |
205 chrome.extension.inIncognitoTab = inIncognitoContext; // deprecated | |
206 chrome.extension.inIncognitoContext = inIncognitoContext; | |
207 | |
208 // Events for when a message channel is opened to our extension. | |
209 chrome.extension.onConnect = new chrome.Event(); | |
210 chrome.extension.onConnectExternal = new chrome.Event(); | |
211 chrome.extension.onRequest = new chrome.Event(); | |
212 chrome.extension.onRequestExternal = new chrome.Event(); | |
213 | |
214 // Opens a message channel to the given target extension, or the current one | |
215 // if unspecified. Returns a Port for message passing. | |
216 chrome.extension.connect = function(targetId_opt, connectInfo_opt) { | |
217 var name = ""; | |
218 var targetId = extensionId; | |
219 var nextArg = 0; | |
220 if (typeof(arguments[nextArg]) == "string") | |
221 targetId = arguments[nextArg++]; | |
222 if (typeof(arguments[nextArg]) == "object") | |
223 name = arguments[nextArg++].name || name; | |
224 if (nextArg != arguments.length) | |
225 throw new Error("Invalid arguments to connect."); | |
226 | |
227 var portId = OpenChannelToExtension(extensionId, targetId, name); | |
228 if (portId >= 0) | |
229 return chromeHidden.Port.createPort(portId, name); | |
230 throw new Error("Error connecting to extension '" + targetId + "'"); | |
231 }; | |
232 | |
233 chrome.extension.sendRequest = | |
234 function(targetId_opt, request, responseCallback_opt) { | |
235 var targetId = extensionId; | |
236 var responseCallback = null; | |
237 var lastArg = arguments.length - 1; | |
238 if (typeof(arguments[lastArg]) == "function") | |
239 responseCallback = arguments[lastArg--]; | |
240 request = arguments[lastArg--]; | |
241 if (lastArg >= 0 && typeof(arguments[lastArg]) == "string") | |
242 targetId = arguments[lastArg--]; | |
243 if (lastArg != -1) | |
244 throw new Error("Invalid arguments to sendRequest."); | |
245 | |
246 var port = chrome.extension.connect(targetId, | |
247 {name: chromeHidden.kRequestChannel}); | |
248 port.postMessage(request); | |
249 port.onDisconnect.addListener(function() { | |
250 // For onDisconnects, we only notify the callback if there was an error | |
251 try { | |
252 if (chrome.extension.lastError && responseCallback) | |
253 responseCallback(); | |
254 } finally { | |
255 port = null; | |
256 } | |
257 }); | |
258 port.onMessage.addListener(function(response) { | |
259 try { | |
260 if (responseCallback) | |
261 responseCallback(response); | |
262 } finally { | |
263 port.disconnect(); | |
264 port = null; | |
265 } | |
266 }); | |
267 }; | |
268 | |
269 // Returns a resource URL that can be used to fetch a resource from this | |
270 // extension. | |
271 chrome.extension.getURL = function(path) { | |
272 path = String(path); | |
273 if (!path.length || path[0] != "/") | |
274 path = "/" + path; | |
275 return "chrome-extension://" + extensionId + path; | |
276 }; | |
277 | |
278 chrome.i18n = chrome.i18n || {}; | |
279 chrome.i18n.getMessage = function(message_name, placeholders) { | |
280 return GetL10nMessage(message_name, placeholders, extensionId); | |
281 }; | |
282 | |
283 if (!isExtensionProcess) | |
284 setupApiStubs(); | |
285 }); | |
286 | |
287 var notSupportedSuffix = " can only be used in extension processes. " + | |
288 "See the content scripts documentation for more details."; | |
289 | |
290 // Setup to throw an error message when trying to access |name| on the chrome | |
291 // object. The |name| can be a dot-separated path. | |
292 function createStub(name) { | |
293 var module = chrome; | |
294 var parts = name.split("."); | |
295 for (var i = 0; i < parts.length - 1; i++) { | |
296 var nextPart = parts[i]; | |
297 // Make sure an object at the path so far is defined. | |
298 module[nextPart] = module[nextPart] || {}; | |
299 module = module[nextPart]; | |
300 } | |
301 var finalPart = parts[parts.length-1]; | |
302 module.__defineGetter__(finalPart, function() { | |
303 throw new Error("chrome." + name + notSupportedSuffix); | |
304 }); | |
305 } | |
306 | |
307 // Sets up stubs to throw a better error message for the common case of | |
308 // developers trying to call extension API's that aren't allowed to be | |
309 // called from content scripts. | |
310 function setupApiStubs() { | |
311 // TODO(asargent) It would be nice to eventually generate this | |
312 // programmatically from extension_api.json (there is already a browser test | |
313 // that should prevent it from getting stale). | |
314 var privileged = [ | |
315 // Entire namespaces. | |
316 "bookmarks", | |
317 "browserAction", | |
318 "chromeAuthPrivate", | |
319 "chromePrivate", | |
320 "chromeosInfoPrivate", | |
321 "contentSettings", | |
322 "contextMenus", | |
323 "cookies", | |
324 "devtools", | |
325 "experimental.accessibility", | |
326 "experimental.app", | |
327 "experimental.bookmarkManager", | |
328 "experimental.clear", | |
329 "experimental.clipboard", | |
330 "experimental.debugger", | |
331 "experimental.downloads", | |
332 "experimental.extension", | |
333 "experimental.infobars", | |
334 "experimental.input", | |
335 "experimental.inputUI", | |
336 "experimental.metrics", | |
337 "experimental.settings", | |
338 "experimental.popup", | |
339 "experimental.processes", | |
340 "experimental.privacy", | |
341 "experimental.rlz", | |
342 "experimental.savePage", | |
343 "experimental.sidebar", | |
344 "experimental.speechInput", | |
345 "experimental.topSites", | |
346 "experimental.webRequest", | |
347 "fileBrowserHandler", | |
348 "fileBrowserPrivate", | |
349 "fileSystem", | |
350 "history", | |
351 "idle", | |
352 "inputMethodPrivate", | |
353 "management", | |
354 "mediaPlayerPrivate", | |
355 "omnibox", | |
356 "pageAction", | |
357 "pageActions", | |
358 "permissions", | |
359 "proxy", | |
360 "tabs", | |
361 "test", | |
362 "tts", | |
363 "ttsEngine", | |
364 "types", | |
365 "webNavigation", | |
366 "webSocketProxyPrivate", | |
367 "webstorePrivate", | |
368 "windows", | |
369 | |
370 // Functions/events/properties within the extension namespace. | |
371 "extension.getBackgroundPage", | |
372 "extension.getExtensionTabs", | |
373 "extension.getViews", | |
374 "extension.isAllowedIncognitoAccess", | |
375 "extension.isAllowedFileSchemeAccess", | |
376 "extension.onConnectExternal", | |
377 "extension.onRequestExternal", | |
378 "extension.setUpdateUrlData", | |
379 "i18n.getAcceptLanguages" | |
380 ]; | |
381 for (var i = 0; i < privileged.length; i++) { | |
382 createStub(privileged[i]); | |
383 } | |
384 } | |
385 | |
386 })(); | |
OLD | NEW |