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 toolstrips. | |
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 port.postMessage(response); | |
101 port = null; | |
102 }); | |
103 }); | |
104 } | |
105 return; | |
106 } | |
107 | |
108 var connectEvent = (isExternal ? | |
109 chrome.extension.onConnectExternal : chrome.extension.onConnect); | |
110 if (connectEvent.hasListeners()) { | |
111 var port = chromeHidden.Port.createPort(portId, channelName); | |
112 port.sender = sender; | |
113 // TODO(EXTENSIONS_DEPRECATED): port.tab is obsolete. | |
114 port.tab = port.sender.tab; | |
115 | |
116 connectEvent.dispatch(port); | |
117 } | |
118 }; | |
119 | |
120 // Called by native code when a channel has been closed. | |
121 chromeHidden.Port.dispatchOnDisconnect = function( | |
122 portId, connectionInvalid) { | |
123 var port = ports[portId]; | |
124 if (port) { | |
125 // Update the renderer's port bookkeeping, without notifying the browser. | |
126 CloseChannel(portId, false); | |
127 if (connectionInvalid) { | |
128 var errorMsg = | |
129 "Could not establish connection. Receiving end does not exist."; | |
130 chrome.extension.lastError = {"message": errorMsg}; | |
131 console.error("Port error: " + errorMsg); | |
132 } | |
133 try { | |
134 port.onDisconnect.dispatch(port); | |
135 } finally { | |
136 port.destroy_(); | |
137 delete chrome.extension.lastError; | |
138 } | |
139 } | |
140 }; | |
141 | |
142 // Called by native code when a message has been sent to the given port. | |
143 chromeHidden.Port.dispatchOnMessage = function(msg, portId) { | |
144 var port = ports[portId]; | |
145 if (port) { | |
146 if (msg) { | |
147 msg = chromeHidden.JSON.parse(msg); | |
148 } | |
149 port.onMessage.dispatch(msg, port); | |
150 } | |
151 }; | |
152 | |
153 // Sends a message asynchronously to the context on the other end of this | |
154 // port. | |
155 chrome.Port.prototype.postMessage = function(msg) { | |
156 // JSON.stringify doesn't support a root object which is undefined. | |
157 if (msg === undefined) | |
158 msg = null; | |
159 PostMessage(this.portId_, chromeHidden.JSON.stringify(msg)); | |
160 }; | |
161 | |
162 // Disconnects the port from the other end. | |
163 chrome.Port.prototype.disconnect = function() { | |
164 CloseChannel(this.portId_, true); | |
165 this.destroy_(); | |
166 }; | |
167 | |
168 chrome.Port.prototype.destroy_ = function() { | |
169 var portId = this.portId_; | |
170 | |
171 this.onDisconnect.destroy_(); | |
172 this.onMessage.destroy_(); | |
173 | |
174 PortRelease(portId); | |
175 chromeHidden.onUnload.removeListener(portReleasers[portId]); | |
176 | |
177 delete ports[portId]; | |
178 delete portReleasers[portId]; | |
179 }; | |
180 | |
181 // This function is called on context initialization for both content scripts | |
182 // and extension contexts. | |
183 chromeHidden.onLoad.addListener(function(extensionId, isExtensionProcess, | |
184 inIncognitoContext) { | |
185 chromeHidden.extensionId = extensionId; | |
186 | |
187 chrome.extension = chrome.extension || {}; | |
188 chrome.self = chrome.extension; | |
189 | |
190 chrome.extension.inIncognitoTab = inIncognitoContext; // deprecated | |
191 chrome.extension.inIncognitoContext = inIncognitoContext; | |
192 | |
193 // Events for when a message channel is opened to our extension. | |
194 chrome.extension.onConnect = new chrome.Event(); | |
195 chrome.extension.onConnectExternal = new chrome.Event(); | |
196 chrome.extension.onRequest = new chrome.Event(); | |
197 chrome.extension.onRequestExternal = new chrome.Event(); | |
198 | |
199 // Opens a message channel to the given target extension, or the current one | |
200 // if unspecified. Returns a Port for message passing. | |
201 chrome.extension.connect = function(targetId_opt, connectInfo_opt) { | |
202 var name = ""; | |
203 var targetId = extensionId; | |
204 var nextArg = 0; | |
205 if (typeof(arguments[nextArg]) == "string") | |
206 targetId = arguments[nextArg++]; | |
207 if (typeof(arguments[nextArg]) == "object") | |
208 name = arguments[nextArg++].name || name; | |
209 if (nextArg != arguments.length) | |
210 throw new Error("Invalid arguments to connect."); | |
211 | |
212 var portId = OpenChannelToExtension(extensionId, targetId, name); | |
213 if (portId >= 0) | |
214 return chromeHidden.Port.createPort(portId, name); | |
215 throw new Error("Error connecting to extension '" + targetId + "'"); | |
216 }; | |
217 | |
218 chrome.extension.sendRequest = | |
219 function(targetId_opt, request, responseCallback_opt) { | |
220 var targetId = extensionId; | |
221 var responseCallback = null; | |
222 var lastArg = arguments.length - 1; | |
223 if (typeof(arguments[lastArg]) == "function") | |
224 responseCallback = arguments[lastArg--]; | |
225 request = arguments[lastArg--]; | |
226 if (lastArg >= 0 && typeof(arguments[lastArg]) == "string") | |
227 targetId = arguments[lastArg--]; | |
228 if (lastArg != -1) | |
229 throw new Error("Invalid arguments to sendRequest."); | |
230 | |
231 var port = chrome.extension.connect(targetId, | |
232 {name: chromeHidden.kRequestChannel}); | |
233 port.postMessage(request); | |
234 port.onDisconnect.addListener(function() { | |
235 // For onDisconnects, we only notify the callback if there was an error | |
236 try { | |
237 if (chrome.extension.lastError && responseCallback) | |
238 responseCallback(); | |
239 } finally { | |
240 port = null; | |
241 } | |
242 }); | |
243 port.onMessage.addListener(function(response) { | |
244 try { | |
245 if (responseCallback) | |
246 responseCallback(response); | |
247 } finally { | |
248 port.disconnect(); | |
249 port = null; | |
250 } | |
251 }); | |
252 }; | |
253 | |
254 // Returns a resource URL that can be used to fetch a resource from this | |
255 // extension. | |
256 chrome.extension.getURL = function(path) { | |
257 path = String(path); | |
258 if (!path.length || path[0] != "/") | |
259 path = "/" + path; | |
260 return "chrome-extension://" + extensionId + path; | |
261 }; | |
262 | |
263 chrome.i18n = chrome.i18n || {}; | |
264 chrome.i18n.getMessage = function(message_name, placeholders) { | |
265 return GetL10nMessage(message_name, placeholders, extensionId); | |
266 }; | |
267 | |
268 if (!isExtensionProcess) | |
269 setupApiStubs(); | |
270 }); | |
271 | |
272 var notSupportedSuffix = " can only be used in extension processes. " + | |
273 "See the content scripts documentation for more details."; | |
274 | |
275 // Setup to throw an error message when trying to access |name| on the chrome | |
276 // object. The |name| can be a dot-separated path. | |
277 function createStub(name) { | |
278 var module = chrome; | |
279 var parts = name.split("."); | |
280 for (var i = 0; i < parts.length - 1; i++) { | |
281 var nextPart = parts[i]; | |
282 // Make sure an object at the path so far is defined. | |
283 module[nextPart] = module[nextPart] || {}; | |
284 module = module[nextPart]; | |
285 } | |
286 var finalPart = parts[parts.length-1]; | |
287 module.__defineGetter__(finalPart, function() { | |
288 throw new Error("chrome." + name + notSupportedSuffix); | |
289 }); | |
290 } | |
291 | |
292 // Sets up stubs to throw a better error message for the common case of | |
293 // developers trying to call extension API's that aren't allowed to be | |
294 // called from content scripts. | |
295 function setupApiStubs() { | |
296 // TODO(asargent) It would be nice to eventually generate this | |
297 // programmatically from extension_api.json (there is already a browser test | |
298 // that should prevent it from getting stale). | |
299 var privileged = [ | |
300 // Entire namespaces. | |
301 "bookmarks", | |
302 "browserAction", | |
303 "chromeAuthPrivate", | |
304 "chromePrivate", | |
305 "chromeosInfoPrivate", | |
306 "contextMenus", | |
307 "cookies", | |
308 "devtools", | |
309 "experimental.accessibility", | |
310 "experimental.app", | |
311 "experimental.bookmarkManager", | |
312 "experimental.clear", | |
313 "experimental.contentSettings", | |
314 "experimental.debugger", | |
315 "experimental.downloads", | |
316 "experimental.extension", | |
317 "experimental.infobars", | |
318 "experimental.input", | |
319 "experimental.inputUI", | |
320 "experimental.metrics", | |
321 "experimental.permissions", | |
322 "experimental.settings", | |
323 "experimental.popup", | |
324 "experimental.processes", | |
325 "experimental.privacy", | |
326 "experimental.rlz", | |
327 "experimental.sidebar", | |
328 "experimental.webNavigation", | |
329 "experimental.webRequest", | |
330 "fileBrowserHandler", | |
331 "fileBrowserPrivate", | |
332 "fileSystem", | |
333 "history", | |
334 "idle", | |
335 "inputMethodPrivate", | |
336 "management", | |
337 "mediaPlayerPrivate", | |
338 "omnibox", | |
339 "pageAction", | |
340 "pageActions", | |
341 "proxy", | |
342 "tabs", | |
343 "test", | |
344 "toolstrip", | |
345 "tts", | |
346 "ttsEngine", | |
347 "types", | |
348 "webSocketProxyPrivate", | |
349 "webstorePrivate", | |
350 "windows", | |
351 | |
352 // Functions/events/properties within the extension namespace. | |
353 "extension.getBackgroundPage", | |
354 "extension.getExtensionTabs", | |
355 "extension.getToolstrips", | |
356 "extension.getViews", | |
357 "extension.isAllowedIncognitoAccess", | |
358 "extension.isAllowedFileSchemeAccess", | |
359 "extension.onConnectExternal", | |
360 "extension.onRequestExternal", | |
361 "extension.setUpdateUrlData", | |
362 "i18n.getAcceptLanguages" | |
363 ]; | |
364 for (var i = 0; i < privileged.length; i++) { | |
365 createStub(privileged[i]); | |
366 } | |
367 } | |
368 | |
369 })(); | |
OLD | NEW |