OLD | NEW |
| (Empty) |
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 // Generates the chrome.* API bindings from a list of schemas. | |
6 | |
7 // TODO(battre): cleanup the usage of packages everywhere, as described here | |
8 // http://codereview.chromium.org/10392008/diff/38/chrome/renderer/resources/e
xtensions/schema_generated_bindings.js | |
9 | |
10 require('json_schema'); | |
11 require('event_bindings'); | |
12 var GetExtensionAPIDefinition = | |
13 requireNative('apiDefinitions').GetExtensionAPIDefinition; | |
14 var sendRequest = require('sendRequest').sendRequest; | |
15 var utils = require('utils'); | |
16 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); | |
17 var schemaUtils = require('schemaUtils'); | |
18 | |
19 // The object to generate the bindings for "internal" APIs in, so that | |
20 // extensions can't directly call them (without access to chromeHidden), | |
21 // but are still needed for internal mechanisms of extensions (e.g. events). | |
22 // | |
23 // This is distinct to the "*Private" APIs which are controlled via | |
24 // having strict permissions and aren't generated *anywhere* unless needed. | |
25 var internalAPIs = {}; | |
26 chromeHidden.internalAPIs = internalAPIs; | |
27 | |
28 // Stores the name and definition of each API function, with methods to | |
29 // modify their behaviour (such as a custom way to handle requests to the | |
30 // API, a custom callback, etc). | |
31 function APIFunctions() { | |
32 this._apiFunctions = {}; | |
33 this._unavailableApiFunctions = {}; | |
34 } | |
35 APIFunctions.prototype.register = function(apiName, apiFunction) { | |
36 this._apiFunctions[apiName] = apiFunction; | |
37 }; | |
38 // Registers a function as existing but not available, meaning that calls to | |
39 // the set* methods that reference this function should be ignored rather | |
40 // than throwing Errors. | |
41 APIFunctions.prototype.registerUnavailable = function(apiName) { | |
42 this._unavailableApiFunctions[apiName] = apiName; | |
43 }; | |
44 APIFunctions.prototype._setHook = | |
45 function(apiName, propertyName, customizedFunction) { | |
46 if (this._unavailableApiFunctions.hasOwnProperty(apiName)) | |
47 return; | |
48 if (!this._apiFunctions.hasOwnProperty(apiName)) | |
49 throw new Error('Tried to set hook for unknown API "' + apiName + '"'); | |
50 this._apiFunctions[apiName][propertyName] = customizedFunction; | |
51 }; | |
52 APIFunctions.prototype.setHandleRequest = | |
53 function(apiName, customizedFunction) { | |
54 return this._setHook(apiName, 'handleRequest', customizedFunction); | |
55 }; | |
56 APIFunctions.prototype.setUpdateArgumentsPostValidate = | |
57 function(apiName, customizedFunction) { | |
58 return this._setHook( | |
59 apiName, 'updateArgumentsPostValidate', customizedFunction); | |
60 }; | |
61 APIFunctions.prototype.setUpdateArgumentsPreValidate = | |
62 function(apiName, customizedFunction) { | |
63 return this._setHook( | |
64 apiName, 'updateArgumentsPreValidate', customizedFunction); | |
65 }; | |
66 APIFunctions.prototype.setCustomCallback = | |
67 function(apiName, customizedFunction) { | |
68 return this._setHook(apiName, 'customCallback', customizedFunction); | |
69 }; | |
70 | |
71 var apiFunctions = new APIFunctions(); | |
72 | |
73 // Wraps the calls to the set* methods of APIFunctions with the namespace of | |
74 // an API, and validates that all calls to set* methods aren't prefixed with | |
75 // a namespace. | |
76 // | |
77 // For example, if constructed with 'browserAction', a call to | |
78 // handleRequest('foo') will be transformed into | |
79 // handleRequest('browserAction.foo'). | |
80 // | |
81 // Likewise, if a call to handleRequest is called with 'browserAction.foo', | |
82 // it will throw an error. | |
83 // | |
84 // These help with isolating custom bindings from each other. | |
85 function NamespacedAPIFunctions(namespace, delegate) { | |
86 var self = this; | |
87 function wrap(methodName) { | |
88 self[methodName] = function(apiName, customizedFunction) { | |
89 var prefix = namespace + '.'; | |
90 if (apiName.indexOf(prefix) === 0) { | |
91 throw new Error(methodName + ' called with "' + apiName + | |
92 '" which has a "' + prefix + '" prefix. ' + | |
93 'This is unnecessary and must be left out.'); | |
94 } | |
95 return delegate[methodName].call(delegate, | |
96 prefix + apiName, customizedFunction); | |
97 }; | |
98 } | |
99 | |
100 wrap('contains'); | |
101 wrap('setHandleRequest'); | |
102 wrap('setUpdateArgumentsPostValidate'); | |
103 wrap('setUpdateArgumentsPreValidate'); | |
104 wrap('setCustomCallback'); | |
105 } | |
106 | |
107 // | |
108 // The API through which the ${api_name}_custom_bindings.js files customize | |
109 // their API bindings beyond what can be generated. | |
110 // | |
111 // There are 2 types of customizations available: those which are required in | |
112 // order to do the schema generation (registerCustomEvent and | |
113 // registerCustomType), and those which can only run after the bindings have | |
114 // been generated (registerCustomHook). | |
115 // | |
116 | |
117 // Registers a custom event type for the API identified by |namespace|. | |
118 // |event| is the event's constructor. | |
119 var customEvents = {}; | |
120 chromeHidden.registerCustomEvent = function(namespace, event) { | |
121 if (typeof(namespace) !== 'string') { | |
122 throw new Error("registerCustomEvent requires the namespace of the " + | |
123 "API as its first argument"); | |
124 } | |
125 customEvents[namespace] = event; | |
126 }; | |
127 | |
128 // Registers a function |hook| to run after the schema for all APIs has been | |
129 // generated. The hook is passed as its first argument an "API" object to | |
130 // interact with, and second the current extension ID. See where | |
131 // |customHooks| is used. | |
132 var customHooks = {}; | |
133 chromeHidden.registerCustomHook = function(namespace, fn) { | |
134 if (typeof(namespace) !== 'string') { | |
135 throw new Error("registerCustomHook requires the namespace of the " + | |
136 "API as its first argument"); | |
137 } | |
138 customHooks[namespace] = fn; | |
139 }; | |
140 | |
141 function CustomBindingsObject() { | |
142 } | |
143 CustomBindingsObject.prototype.setSchema = function(schema) { | |
144 // The functions in the schema are in list form, so we move them into a | |
145 // dictionary for easier access. | |
146 var self = this; | |
147 self.functionSchemas = {}; | |
148 schema.functions.forEach(function(f) { | |
149 self.functionSchemas[f.name] = { | |
150 name: f.name, | |
151 definition: f | |
152 } | |
153 }); | |
154 }; | |
155 | |
156 // Registers a custom type referenced via "$ref" fields in the API schema | |
157 // JSON. | |
158 var customTypes = {}; | |
159 chromeHidden.registerCustomType = function(typeName, customTypeFactory) { | |
160 var customType = customTypeFactory(); | |
161 customType.prototype = new CustomBindingsObject(); | |
162 customTypes[typeName] = customType; | |
163 }; | |
164 | |
165 // Get the platform from navigator.appVersion. | |
166 function getPlatform() { | |
167 var platforms = [ | |
168 [/CrOS Touch/, "chromeos touch"], | |
169 [/CrOS/, "chromeos"], | |
170 [/Linux/, "linux"], | |
171 [/Mac/, "mac"], | |
172 [/Win/, "win"], | |
173 ]; | |
174 | |
175 for (var i = 0; i < platforms.length; i++) { | |
176 if (platforms[i][0].test(navigator.appVersion)) { | |
177 return platforms[i][1]; | |
178 } | |
179 } | |
180 return "unknown"; | |
181 } | |
182 | |
183 function isPlatformSupported(schemaNode, platform) { | |
184 return !schemaNode.platforms || | |
185 schemaNode.platforms.indexOf(platform) > -1; | |
186 } | |
187 | |
188 function isManifestVersionSupported(schemaNode, manifestVersion) { | |
189 return !schemaNode.maximumManifestVersion || | |
190 manifestVersion <= schemaNode.maximumManifestVersion; | |
191 } | |
192 | |
193 function isSchemaNodeSupported(schemaNode, platform, manifestVersion) { | |
194 return isPlatformSupported(schemaNode, platform) && | |
195 isManifestVersionSupported(schemaNode, manifestVersion); | |
196 } | |
197 | |
198 chromeHidden.onLoad.addListener(function(extensionId, | |
199 contextType, | |
200 isIncognitoProcess, | |
201 manifestVersion) { | |
202 var apiDefinitions = GetExtensionAPIDefinition(); | |
203 | |
204 // Read api definitions and setup api functions in the chrome namespace. | |
205 var platform = getPlatform(); | |
206 | |
207 apiDefinitions.forEach(function(apiDef) { | |
208 // TODO(kalman): Remove this, or refactor schema_generated_bindings.js so | |
209 // that it isn't necessary. For now, chrome.app and chrome.webstore are | |
210 // entirely handwritten. | |
211 if (['app', 'webstore'].indexOf(apiDef.namespace) >= 0) | |
212 return; | |
213 | |
214 if (!isSchemaNodeSupported(apiDef, platform, manifestVersion)) | |
215 return; | |
216 | |
217 // See comment on internalAPIs at the top. | |
218 var mod = apiDef.internal ? internalAPIs : chrome; | |
219 | |
220 var namespaces = apiDef.namespace.split('.'); | |
221 for (var index = 0, name; name = namespaces[index]; index++) { | |
222 mod[name] = mod[name] || {}; | |
223 mod = mod[name]; | |
224 } | |
225 | |
226 // Add types to global schemaValidator | |
227 if (apiDef.types) { | |
228 apiDef.types.forEach(function(t) { | |
229 if (!isSchemaNodeSupported(t, platform, manifestVersion)) | |
230 return; | |
231 | |
232 schemaUtils.schemaValidator.addTypes(t); | |
233 if (t.type == 'object' && customTypes[t.id]) { | |
234 customTypes[t.id].prototype.setSchema(t); | |
235 } | |
236 }); | |
237 } | |
238 | |
239 // Returns whether access to the content of a schema should be denied, | |
240 // based on the presence of "unprivileged" and whether this is an | |
241 // extension process (versus e.g. a content script). | |
242 function isSchemaAccessAllowed(itemSchema) { | |
243 return (contextType == 'BLESSED_EXTENSION') || | |
244 apiDef.unprivileged || | |
245 itemSchema.unprivileged; | |
246 } | |
247 | |
248 // Adds a getter that throws an access denied error to object |mod| | |
249 // for property |name|. | |
250 function addUnprivilegedAccessGetter(mod, name) { | |
251 mod.__defineGetter__(name, function() { | |
252 throw new Error( | |
253 '"' + name + '" can only be used in extension processes. See ' + | |
254 'the content scripts documentation for more details.'); | |
255 }); | |
256 } | |
257 | |
258 // Setup Functions. | |
259 if (apiDef.functions) { | |
260 apiDef.functions.forEach(function(functionDef) { | |
261 if (functionDef.name in mod) { | |
262 throw new Error('Function ' + functionDef.name + | |
263 ' already defined in ' + apiDef.namespace); | |
264 } | |
265 | |
266 var apiFunctionName = apiDef.namespace + "." + functionDef.name; | |
267 | |
268 if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) { | |
269 apiFunctions.registerUnavailable(apiFunctionName); | |
270 return; | |
271 } | |
272 if (!isSchemaAccessAllowed(functionDef)) { | |
273 apiFunctions.registerUnavailable(apiFunctionName); | |
274 addUnprivilegedAccessGetter(mod, functionDef.name); | |
275 return; | |
276 } | |
277 | |
278 var apiFunction = {}; | |
279 apiFunction.definition = functionDef; | |
280 apiFunction.name = apiFunctionName; | |
281 | |
282 // TODO(aa): It would be best to run this in a unit test, but in order | |
283 // to do that we would need to better factor this code so that it | |
284 // doesn't depend on so much v8::Extension machinery. | |
285 if (chromeHidden.validateAPI && | |
286 schemaUtils.isFunctionSignatureAmbiguous( | |
287 apiFunction.definition)) { | |
288 throw new Error( | |
289 apiFunction.name + ' has ambiguous optional arguments. ' + | |
290 'To implement custom disambiguation logic, add ' + | |
291 '"allowAmbiguousOptionalArguments" to the function\'s schema.'); | |
292 } | |
293 | |
294 apiFunctions.register(apiFunction.name, apiFunction); | |
295 | |
296 mod[functionDef.name] = (function() { | |
297 var args = Array.prototype.slice.call(arguments); | |
298 if (this.updateArgumentsPreValidate) | |
299 args = this.updateArgumentsPreValidate.apply(this, args); | |
300 | |
301 args = schemaUtils.normalizeArgumentsAndValidate(args, this); | |
302 if (this.updateArgumentsPostValidate) | |
303 args = this.updateArgumentsPostValidate.apply(this, args); | |
304 | |
305 var retval; | |
306 if (this.handleRequest) { | |
307 retval = this.handleRequest.apply(this, args); | |
308 } else { | |
309 var optArgs = { | |
310 customCallback: this.customCallback | |
311 }; | |
312 retval = sendRequest(this.name, args, | |
313 this.definition.parameters, | |
314 optArgs); | |
315 } | |
316 | |
317 // Validate return value if defined - only in debug. | |
318 if (chromeHidden.validateCallbacks && | |
319 this.definition.returns) { | |
320 schemaUtils.validate([retval], [this.definition.returns]); | |
321 } | |
322 return retval; | |
323 }).bind(apiFunction); | |
324 }); | |
325 } | |
326 | |
327 // Setup Events | |
328 if (apiDef.events) { | |
329 apiDef.events.forEach(function(eventDef) { | |
330 if (eventDef.name in mod) { | |
331 throw new Error('Event ' + eventDef.name + | |
332 ' already defined in ' + apiDef.namespace); | |
333 } | |
334 if (!isSchemaNodeSupported(eventDef, platform, manifestVersion)) | |
335 return; | |
336 if (!isSchemaAccessAllowed(eventDef)) { | |
337 addUnprivilegedAccessGetter(mod, eventDef.name); | |
338 return; | |
339 } | |
340 | |
341 var eventName = apiDef.namespace + "." + eventDef.name; | |
342 var customEvent = customEvents[apiDef.namespace]; | |
343 var options = eventDef.options || {}; | |
344 | |
345 if (eventDef.filters && eventDef.filters.length > 0) | |
346 options.supportsFilters = true; | |
347 | |
348 if (customEvent) { | |
349 mod[eventDef.name] = new customEvent( | |
350 eventName, eventDef.parameters, eventDef.extraParameters, | |
351 options); | |
352 } else if (eventDef.anonymous) { | |
353 mod[eventDef.name] = new chrome.Event(); | |
354 } else { | |
355 mod[eventDef.name] = new chrome.Event( | |
356 eventName, eventDef.parameters, options); | |
357 } | |
358 }); | |
359 } | |
360 | |
361 function addProperties(m, parentDef) { | |
362 var properties = parentDef.properties; | |
363 if (!properties) | |
364 return; | |
365 | |
366 utils.forEach(properties, function(propertyName, propertyDef) { | |
367 if (propertyName in m) | |
368 return; // TODO(kalman): be strict like functions/events somehow. | |
369 if (!isSchemaNodeSupported(propertyDef, platform, manifestVersion)) | |
370 return; | |
371 if (!isSchemaAccessAllowed(propertyDef)) { | |
372 addUnprivilegedAccessGetter(m, propertyName); | |
373 return; | |
374 } | |
375 | |
376 var value = propertyDef.value; | |
377 if (value) { | |
378 // Values may just have raw types as defined in the JSON, such | |
379 // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here. | |
380 // TODO(kalman): enforce that things with a "value" property can't | |
381 // define their own types. | |
382 var type = propertyDef.type || typeof(value); | |
383 if (type === 'integer' || type === 'number') { | |
384 value = parseInt(value); | |
385 } else if (type === 'boolean') { | |
386 value = value === "true"; | |
387 } else if (propertyDef["$ref"]) { | |
388 var constructor = customTypes[propertyDef["$ref"]]; | |
389 if (!constructor) | |
390 throw new Error("No custom binding for " + propertyDef["$ref"]); | |
391 var args = value; | |
392 // For an object propertyDef, |value| is an array of constructor | |
393 // arguments, but we want to pass the arguments directly (i.e. | |
394 // not as an array), so we have to fake calling |new| on the | |
395 // constructor. | |
396 value = { __proto__: constructor.prototype }; | |
397 constructor.apply(value, args); | |
398 // Recursively add properties. | |
399 addProperties(value, propertyDef); | |
400 } else if (type === 'object') { | |
401 // Recursively add properties. | |
402 addProperties(value, propertyDef); | |
403 } else if (type !== 'string') { | |
404 throw new Error("NOT IMPLEMENTED (extension_api.json error): " + | |
405 "Cannot parse values for type \"" + type + "\""); | |
406 } | |
407 m[propertyName] = value; | |
408 } | |
409 }); | |
410 } | |
411 | |
412 addProperties(mod, apiDef); | |
413 }); | |
414 | |
415 // Run the non-declarative custom hooks after all the schemas have been | |
416 // generated, in case hooks depend on other APIs being available. | |
417 apiDefinitions.forEach(function(apiDef) { | |
418 if (!isSchemaNodeSupported(apiDef, platform, manifestVersion)) | |
419 return; | |
420 | |
421 var hook = customHooks[apiDef.namespace]; | |
422 if (!hook) | |
423 return; | |
424 | |
425 // Pass through the public API of schema_generated_bindings, to be used | |
426 // by custom bindings JS files. Create a new one so that bindings can't | |
427 // interfere with each other. | |
428 hook({ | |
429 apiFunctions: new NamespacedAPIFunctions(apiDef.namespace, | |
430 apiFunctions), | |
431 apiDefinitions: apiDefinitions, | |
432 }, extensionId, contextType); | |
433 }); | |
434 | |
435 if (chrome.test) | |
436 chrome.test.getApiDefinitions = GetExtensionAPIDefinition; | |
437 }); | |
OLD | NEW |