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.
5 var Event
= require('event_bindings').Event
;
6 var forEach
= require('utils').forEach
;
7 var GetAvailability
= requireNative('v8_context').GetAvailability
;
8 var exceptionHandler
= require('uncaught_exception_handler');
9 var lastError
= require('lastError');
10 var logActivity
= requireNative('activityLogger');
11 var logging
= requireNative('logging');
12 var process
= requireNative('process');
13 var schemaRegistry
= requireNative('schema_registry');
14 var schemaUtils
= require('schemaUtils');
15 var utils
= require('utils');
16 var sendRequestHandler
= require('sendRequest');
18 var contextType
= process
.GetContextType();
19 var extensionId
= process
.GetExtensionId();
20 var manifestVersion
= process
.GetManifestVersion();
21 var sendRequest
= sendRequestHandler
.sendRequest
;
23 // Stores the name and definition of each API function, with methods to
24 // modify their behaviour (such as a custom way to handle requests to the
25 // API, a custom callback, etc).
26 function APIFunctions(namespace) {
27 this.apiFunctions_
= {};
28 this.unavailableApiFunctions_
= {};
29 this.namespace = namespace;
32 APIFunctions
.prototype.register = function(apiName
, apiFunction
) {
33 this.apiFunctions_
[apiName
] = apiFunction
;
36 // Registers a function as existing but not available, meaning that calls to
37 // the set* methods that reference this function should be ignored rather
38 // than throwing Errors.
39 APIFunctions
.prototype.registerUnavailable = function(apiName
) {
40 this.unavailableApiFunctions_
[apiName
] = apiName
;
43 APIFunctions
.prototype.setHook_
=
44 function(apiName
, propertyName
, customizedFunction
) {
45 if ($Object
.hasOwnProperty(this.unavailableApiFunctions_
, apiName
))
47 if (!$Object
.hasOwnProperty(this.apiFunctions_
, apiName
))
48 throw new Error('Tried to set hook for unknown API "' + apiName
+ '"');
49 this.apiFunctions_
[apiName
][propertyName
] = customizedFunction
;
52 APIFunctions
.prototype.setHandleRequest
=
53 function(apiName
, customizedFunction
) {
54 var prefix
= this.namespace;
55 return this.setHook_(apiName
, 'handleRequest',
57 var ret
= $Function
.apply(customizedFunction
, this, arguments
);
58 // Logs API calls to the Activity Log if it doesn't go through an
60 if (!sendRequestHandler
.getCalledSendRequest())
61 logActivity
.LogAPICall(extensionId
, prefix
+ "." + apiName
,
62 $Array
.slice(arguments
));
67 APIFunctions
.prototype.setHandleRequestWithPromise
=
68 function(apiName
, customizedFunction
) {
69 var prefix
= this.namespace;
70 return this.setHook_(apiName
, 'handleRequest', function() {
71 var name
= prefix
+ '.' + apiName
;
72 logActivity
.LogAPICall(extensionId
, name
, $Array
.slice(arguments
));
73 var stack
= exceptionHandler
.getExtensionStackTrace();
74 var callback
= arguments
[arguments
.length
- 1];
75 var args
= $Array
.slice(arguments
, 0, arguments
.length
- 1);
76 var keepAlivePromise
= requireAsync('keep_alive').then(function(module
) {
77 return module
.createKeepAlive();
79 $Function
.apply(customizedFunction
, this, args
).then(function(result
) {
81 sendRequestHandler
.safeCallbackApply(name
, {'stack': stack
}, callback
,
84 }).catch(function(error
) {
86 var message
= exceptionHandler
.safeErrorToString(error
, true);
87 lastError
.run(name
, message
, stack
, callback
);
90 keepAlivePromise
.then(function(keepAlive
) {
97 APIFunctions
.prototype.setUpdateArgumentsPostValidate
=
98 function(apiName
, customizedFunction
) {
100 apiName
, 'updateArgumentsPostValidate', customizedFunction
);
103 APIFunctions
.prototype.setUpdateArgumentsPreValidate
=
104 function(apiName
, customizedFunction
) {
105 return this.setHook_(
106 apiName
, 'updateArgumentsPreValidate', customizedFunction
);
109 APIFunctions
.prototype.setCustomCallback
=
110 function(apiName
, customizedFunction
) {
111 return this.setHook_(apiName
, 'customCallback', customizedFunction
);
114 function CustomBindingsObject() {
117 CustomBindingsObject
.prototype.setSchema = function(schema
) {
118 // The functions in the schema are in list form, so we move them into a
119 // dictionary for easier access.
121 self
.functionSchemas
= {};
122 $Array
.forEach(schema
.functions
, function(f
) {
123 self
.functionSchemas
[f
.name
] = {
130 // Get the platform from navigator.appVersion.
131 function getPlatform() {
133 [/CrOS Touch/, "chromeos touch"],
134 [/CrOS/, "chromeos"],
140 for (var i
= 0; i
< platforms
.length
; i
++) {
141 if ($RegExp
.test(platforms
[i
][0], navigator
.appVersion
)) {
142 return platforms
[i
][1];
148 function isPlatformSupported(schemaNode
, platform
) {
149 return !schemaNode
.platforms
||
150 $Array
.indexOf(schemaNode
.platforms
, platform
) > -1;
153 function isManifestVersionSupported(schemaNode
, manifestVersion
) {
154 return !schemaNode
.maximumManifestVersion
||
155 manifestVersion
<= schemaNode
.maximumManifestVersion
;
158 function isSchemaNodeSupported(schemaNode
, platform
, manifestVersion
) {
159 return isPlatformSupported(schemaNode
, platform
) &&
160 isManifestVersionSupported(schemaNode
, manifestVersion
);
163 function createCustomType(type
) {
164 var jsModuleName
= type
.js_module
;
165 logging
.CHECK(jsModuleName
, 'Custom type ' + type
.id
+
166 ' has no "js_module" property.');
167 var jsModule
= require(jsModuleName
);
168 logging
.CHECK(jsModule
, 'No module ' + jsModuleName
+ ' found for ' +
170 var customType
= jsModule
[jsModuleName
];
171 logging
.CHECK(customType
, jsModuleName
+ ' must export itself.');
172 customType
.prototype = new CustomBindingsObject();
173 customType
.prototype.setSchema(type
);
177 var platform
= getPlatform();
179 function Binding(schema
) {
180 this.schema_
= schema
;
181 this.apiFunctions_
= new APIFunctions(schema
.namespace);
182 this.customEvent_
= null;
183 this.customHooks_
= [];
186 Binding
.create = function(apiName
) {
187 return new Binding(schemaRegistry
.GetSchema(apiName
));
190 Binding
.prototype = {
191 // The API through which the ${api_name}_custom_bindings.js files customize
192 // their API bindings beyond what can be generated.
194 // There are 2 types of customizations available: those which are required in
195 // order to do the schema generation (registerCustomEvent and
196 // registerCustomType), and those which can only run after the bindings have
197 // been generated (registerCustomHook).
199 // Registers a custom event type for the API identified by |namespace|.
200 // |event| is the event's constructor.
201 registerCustomEvent: function(event
) {
202 this.customEvent_
= event
;
205 // Registers a function |hook| to run after the schema for all APIs has been
206 // generated. The hook is passed as its first argument an "API" object to
207 // interact with, and second the current extension ID. See where
208 // |customHooks| is used.
209 registerCustomHook: function(fn
) {
210 $Array
.push(this.customHooks_
, fn
);
213 // TODO(kalman/cduvall): Refactor this so |runHooks_| is not needed.
214 runHooks_: function(api
) {
215 $Array
.forEach(this.customHooks_
, function(hook
) {
216 if (!isSchemaNodeSupported(this.schema_
, platform
, manifestVersion
))
223 apiFunctions
: this.apiFunctions_
,
224 schema
: this.schema_
,
226 }, extensionId
, contextType
);
230 // Generates the bindings from |this.schema_| and integrates any custom
231 // bindings that might be present.
232 generate: function() {
233 var schema
= this.schema_
;
235 function shouldCheckUnprivileged() {
236 var shouldCheck
= 'unprivileged' in schema
;
240 $Array
.forEach(['functions', 'events'], function(type
) {
241 if ($Object
.hasOwnProperty(schema
, type
)) {
242 $Array
.forEach(schema
[type
], function(node
) {
243 if ('unprivileged' in node
)
251 for (var property
in schema
.properties
) {
252 if ($Object
.hasOwnProperty(schema
, property
) &&
253 'unprivileged' in schema
.properties
[property
]) {
260 var checkUnprivileged
= shouldCheckUnprivileged();
262 // TODO(kalman/cduvall): Make GetAvailability handle this, then delete the
264 if (!isSchemaNodeSupported(schema
, platform
, manifestVersion
)) {
265 console
.error('chrome.' + schema
.namespace + ' is not supported on ' +
266 'this platform or manifest version');
272 var namespaces
= $String
.split(schema
.namespace, '.');
273 for (var index
= 0, name
; name
= namespaces
[index
]; index
++) {
274 mod
[name
] = mod
[name
] || {};
279 $Array
.forEach(schema
.types
, function(t
) {
280 if (!isSchemaNodeSupported(t
, platform
, manifestVersion
))
283 // Add types to global schemaValidator; the types we depend on from
284 // other namespaces will be added as needed.
285 schemaUtils
.schemaValidator
.addTypes(t
);
287 // Generate symbols for enums.
288 var enumValues
= t
['enum'];
290 // Type IDs are qualified with the namespace during compilation,
291 // unfortunately, so remove it here.
292 logging
.DCHECK($String
.substr(t
.id
, 0, schema
.namespace.length
) ==
294 // Note: + 1 because it ends in a '.', e.g., 'fooApi.Type'.
295 var id
= $String
.substr(t
.id
, schema
.namespace.length
+ 1);
297 $Array
.forEach(enumValues
, function(enumValue
) {
298 // Note: enums can be declared either as a list of strings
299 // ['foo', 'bar'] or as a list of objects
300 // [{'name': 'foo'}, {'name': 'bar'}].
301 enumValue
= $Object
.hasOwnProperty(enumValue
, 'name') ?
302 enumValue
.name
: enumValue
;
303 if (enumValue
) { // Avoid setting any empty enums.
304 // Make all properties in ALL_CAPS_STYLE.
305 // Replace myEnum-Foo with my_Enum-Foo:
307 $String
.replace(enumValue
, /([a-z])([A-Z])/g, '$1_$2');
308 // Replace my_Enum-Foo with my_Enum_Foo:
309 propertyName
= $String
.replace(propertyName
, /\W/g, '_');
310 // If the first character is a digit (we know it must be one of
311 // a digit, a letter, or an underscore), precede it with an
313 propertyName
= $String
.replace(propertyName
, /^(\d)/g, '_$1');
314 // Uppercase (replace my_Enum_Foo with MY_ENUM_FOO):
315 propertyName
= $String
.toUpperCase(propertyName
);
316 mod
[id
][propertyName
] = enumValue
;
323 // TODO(cduvall): Take out when all APIs have been converted to features.
324 // Returns whether access to the content of a schema should be denied,
325 // based on the presence of "unprivileged" and whether this is an
326 // extension process (versus e.g. a content script).
327 function isSchemaAccessAllowed(itemSchema
) {
328 return (contextType
== 'BLESSED_EXTENSION') ||
329 schema
.unprivileged
||
330 itemSchema
.unprivileged
;
334 if (schema
.functions
) {
335 $Array
.forEach(schema
.functions
, function(functionDef
) {
336 if (functionDef
.name
in mod
) {
337 throw new Error('Function ' + functionDef
.name
+
338 ' already defined in ' + schema
.namespace);
341 if (!isSchemaNodeSupported(functionDef
, platform
, manifestVersion
)) {
342 this.apiFunctions_
.registerUnavailable(functionDef
.name
);
346 var apiFunction
= {};
347 apiFunction
.definition
= functionDef
;
348 apiFunction
.name
= schema
.namespace + '.' + functionDef
.name
;
350 if (!GetAvailability(apiFunction
.name
).is_available
||
351 (checkUnprivileged
&& !isSchemaAccessAllowed(functionDef
))) {
352 this.apiFunctions_
.registerUnavailable(functionDef
.name
);
356 // TODO(aa): It would be best to run this in a unit test, but in order
357 // to do that we would need to better factor this code so that it
358 // doesn't depend on so much v8::Extension machinery.
359 if (logging
.DCHECK_IS_ON() &&
360 schemaUtils
.isFunctionSignatureAmbiguous(apiFunction
.definition
)) {
362 apiFunction
.name
+ ' has ambiguous optional arguments. ' +
363 'To implement custom disambiguation logic, add ' +
364 '"allowAmbiguousOptionalArguments" to the function\'s schema.');
367 this.apiFunctions_
.register(functionDef
.name
, apiFunction
);
369 mod
[functionDef
.name
] = $Function
.bind(function() {
370 var args
= $Array
.slice(arguments
);
371 if (this.updateArgumentsPreValidate
)
372 args
= $Function
.apply(this.updateArgumentsPreValidate
, this, args
);
374 args
= schemaUtils
.normalizeArgumentsAndValidate(args
, this);
375 if (this.updateArgumentsPostValidate
) {
376 args
= $Function
.apply(this.updateArgumentsPostValidate
,
381 sendRequestHandler
.clearCalledSendRequest();
384 if (this.handleRequest
) {
385 retval
= $Function
.apply(this.handleRequest
, this, args
);
388 customCallback
: this.customCallback
390 retval
= sendRequest(this.name
, args
,
391 this.definition
.parameters
,
394 sendRequestHandler
.clearCalledSendRequest();
396 // Validate return value if in sanity check mode.
397 if (logging
.DCHECK_IS_ON() && this.definition
.returns
)
398 schemaUtils
.validate([retval
], [this.definition
.returns
]);
406 $Array
.forEach(schema
.events
, function(eventDef
) {
407 if (eventDef
.name
in mod
) {
408 throw new Error('Event ' + eventDef
.name
+
409 ' already defined in ' + schema
.namespace);
411 if (!isSchemaNodeSupported(eventDef
, platform
, manifestVersion
))
414 var eventName
= schema
.namespace + "." + eventDef
.name
;
415 if (!GetAvailability(eventName
).is_available
||
416 (checkUnprivileged
&& !isSchemaAccessAllowed(eventDef
))) {
420 var options
= eventDef
.options
|| {};
421 if (eventDef
.filters
&& eventDef
.filters
.length
> 0)
422 options
.supportsFilters
= true;
424 var parameters
= eventDef
.parameters
;
425 if (this.customEvent_
) {
426 mod
[eventDef
.name
] = new this.customEvent_(
427 eventName
, parameters
, eventDef
.extraParameters
, options
);
429 mod
[eventDef
.name
] = new Event(eventName
, parameters
, options
);
434 function addProperties(m
, parentDef
) {
435 var properties
= parentDef
.properties
;
439 forEach(properties
, function(propertyName
, propertyDef
) {
440 if (propertyName
in m
)
441 return; // TODO(kalman): be strict like functions/events somehow.
442 if (!isSchemaNodeSupported(propertyDef
, platform
, manifestVersion
))
444 if (!GetAvailability(schema
.namespace + "." +
445 propertyName
).is_available
||
446 (checkUnprivileged
&& !isSchemaAccessAllowed(propertyDef
))) {
450 var value
= propertyDef
.value
;
452 // Values may just have raw types as defined in the JSON, such
453 // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here.
454 // TODO(kalman): enforce that things with a "value" property can't
455 // define their own types.
456 var type
= propertyDef
.type
|| typeof(value
);
457 if (type
=== 'integer' || type
=== 'number') {
458 value
= parseInt(value
);
459 } else if (type
=== 'boolean') {
460 value
= value
=== 'true';
461 } else if (propertyDef
['$ref']) {
462 var ref
= propertyDef
['$ref'];
463 var type
= utils
.loadTypeSchema(propertyDef
['$ref'], schema
);
464 logging
.CHECK(type
, 'Schema for $ref type ' + ref
+ ' not found');
465 var constructor = createCustomType(type
);
467 // For an object propertyDef, |value| is an array of constructor
468 // arguments, but we want to pass the arguments directly (i.e.
469 // not as an array), so we have to fake calling |new| on the
471 value
= { __proto__
: constructor.prototype };
472 $Function
.apply(constructor, value
, args
);
473 // Recursively add properties.
474 addProperties(value
, propertyDef
);
475 } else if (type
=== 'object') {
476 // Recursively add properties.
477 addProperties(value
, propertyDef
);
478 } else if (type
!== 'string') {
479 throw new Error('NOT IMPLEMENTED (extension_api.json error): ' +
480 'Cannot parse values for type "' + type
+ '"');
482 m
[propertyName
] = value
;
487 addProperties(mod
, schema
);
489 // This generate() call is considered successful if any functions,
490 // properties, or events were created.
491 var success
= ($Object
.keys(mod
).length
> 0);
493 // Special case: webViewRequest is a vacuous API which just copies its
494 // implementation from declarativeWebRequest.
496 // TODO(kalman): This would be unnecessary if we did these checks after the
497 // hooks (i.e. this.runHooks_(mod)). The reason we don't is to be very
498 // conservative with running any JS which might actually be for an API
499 // which isn't available, but this is probably overly cautious given the
500 // C++ is only giving us APIs which are available. FIXME.
501 if (schema
.namespace == 'webViewRequest') {
505 // Special case: runtime.lastError is only occasionally set, so
506 // specifically check its availability.
507 if (schema
.namespace == 'runtime' &&
508 GetAvailability('runtime.lastError').is_available
) {
513 var availability
= GetAvailability(schema
.namespace);
514 // If an API was available it should have been successfully generated.
515 logging
.DCHECK(!availability
.is_available
,
516 schema
.namespace + ' was available but not generated');
517 console
.error('chrome.' + schema
.namespace + ' is not available: ' +
518 availability
.message
);
527 exports
.Binding
= Binding
;