Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(122)

Side by Side Diff: chrome/renderer/resources/extensions/schema_binding_generator.js

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

Powered by Google App Engine
This is Rietveld 408576698