Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / extensions / renderer / resources / binding.js
blobd36f1edf494d4a9b84cd373125ead427a61a03f6
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))
46 return;
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',
56 function() {
57 var ret = $Function.apply(customizedFunction, this, arguments);
58 // Logs API calls to the Activity Log if it doesn't go through an
59 // ExtensionFunction.
60 if (!sendRequestHandler.getCalledSendRequest())
61 logActivity.LogAPICall(extensionId, prefix + "." + apiName,
62 $Array.slice(arguments));
63 return ret;
64 });
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 $Function.apply(customizedFunction, this, args).then(function(result) {
77 sendRequestHandler.safeCallbackApply(
78 name, {'stack': stack}, callback, [result]);
79 }).catch(function(error) {
80 var message = exceptionHandler.safeErrorToString(error, true);
81 lastError.run(name, message, stack, callback);
82 });
83 });
86 APIFunctions.prototype.setUpdateArgumentsPostValidate =
87 function(apiName, customizedFunction) {
88 return this.setHook_(
89 apiName, 'updateArgumentsPostValidate', customizedFunction);
92 APIFunctions.prototype.setUpdateArgumentsPreValidate =
93 function(apiName, customizedFunction) {
94 return this.setHook_(
95 apiName, 'updateArgumentsPreValidate', customizedFunction);
98 APIFunctions.prototype.setCustomCallback =
99 function(apiName, customizedFunction) {
100 return this.setHook_(apiName, 'customCallback', customizedFunction);
103 function CustomBindingsObject() {
106 CustomBindingsObject.prototype.setSchema = function(schema) {
107 // The functions in the schema are in list form, so we move them into a
108 // dictionary for easier access.
109 var self = this;
110 self.functionSchemas = {};
111 $Array.forEach(schema.functions, function(f) {
112 self.functionSchemas[f.name] = {
113 name: f.name,
114 definition: f
119 // Get the platform from navigator.appVersion.
120 function getPlatform() {
121 var platforms = [
122 [/CrOS Touch/, "chromeos touch"],
123 [/CrOS/, "chromeos"],
124 [/Linux/, "linux"],
125 [/Mac/, "mac"],
126 [/Win/, "win"],
129 for (var i = 0; i < platforms.length; i++) {
130 if ($RegExp.test(platforms[i][0], navigator.appVersion)) {
131 return platforms[i][1];
134 return "unknown";
137 function isPlatformSupported(schemaNode, platform) {
138 return !schemaNode.platforms ||
139 $Array.indexOf(schemaNode.platforms, platform) > -1;
142 function isManifestVersionSupported(schemaNode, manifestVersion) {
143 return !schemaNode.maximumManifestVersion ||
144 manifestVersion <= schemaNode.maximumManifestVersion;
147 function isSchemaNodeSupported(schemaNode, platform, manifestVersion) {
148 return isPlatformSupported(schemaNode, platform) &&
149 isManifestVersionSupported(schemaNode, manifestVersion);
152 function createCustomType(type) {
153 var jsModuleName = type.js_module;
154 logging.CHECK(jsModuleName, 'Custom type ' + type.id +
155 ' has no "js_module" property.');
156 var jsModule = require(jsModuleName);
157 logging.CHECK(jsModule, 'No module ' + jsModuleName + ' found for ' +
158 type.id + '.');
159 var customType = jsModule[jsModuleName];
160 logging.CHECK(customType, jsModuleName + ' must export itself.');
161 customType.prototype = new CustomBindingsObject();
162 customType.prototype.setSchema(type);
163 return customType;
166 var platform = getPlatform();
168 function Binding(schema) {
169 this.schema_ = schema;
170 this.apiFunctions_ = new APIFunctions(schema.namespace);
171 this.customEvent_ = null;
172 this.customHooks_ = [];
175 Binding.create = function(apiName) {
176 return new Binding(schemaRegistry.GetSchema(apiName));
179 Binding.prototype = {
180 // The API through which the ${api_name}_custom_bindings.js files customize
181 // their API bindings beyond what can be generated.
183 // There are 2 types of customizations available: those which are required in
184 // order to do the schema generation (registerCustomEvent and
185 // registerCustomType), and those which can only run after the bindings have
186 // been generated (registerCustomHook).
188 // Registers a custom event type for the API identified by |namespace|.
189 // |event| is the event's constructor.
190 registerCustomEvent: function(event) {
191 this.customEvent_ = event;
194 // Registers a function |hook| to run after the schema for all APIs has been
195 // generated. The hook is passed as its first argument an "API" object to
196 // interact with, and second the current extension ID. See where
197 // |customHooks| is used.
198 registerCustomHook: function(fn) {
199 $Array.push(this.customHooks_, fn);
202 // TODO(kalman/cduvall): Refactor this so |runHooks_| is not needed.
203 runHooks_: function(api) {
204 $Array.forEach(this.customHooks_, function(hook) {
205 if (!isSchemaNodeSupported(this.schema_, platform, manifestVersion))
206 return;
208 if (!hook)
209 return;
211 hook({
212 apiFunctions: this.apiFunctions_,
213 schema: this.schema_,
214 compiledApi: api
215 }, extensionId, contextType);
216 }, this);
219 // Generates the bindings from |this.schema_| and integrates any custom
220 // bindings that might be present.
221 generate: function() {
222 var schema = this.schema_;
224 function shouldCheckUnprivileged() {
225 var shouldCheck = 'unprivileged' in schema;
226 if (shouldCheck)
227 return shouldCheck;
229 $Array.forEach(['functions', 'events'], function(type) {
230 if ($Object.hasOwnProperty(schema, type)) {
231 $Array.forEach(schema[type], function(node) {
232 if ('unprivileged' in node)
233 shouldCheck = true;
237 if (shouldCheck)
238 return shouldCheck;
240 for (var property in schema.properties) {
241 if ($Object.hasOwnProperty(schema, property) &&
242 'unprivileged' in schema.properties[property]) {
243 shouldCheck = true;
244 break;
247 return shouldCheck;
249 var checkUnprivileged = shouldCheckUnprivileged();
251 // TODO(kalman/cduvall): Make GetAvailability handle this, then delete the
252 // supporting code.
253 if (!isSchemaNodeSupported(schema, platform, manifestVersion)) {
254 console.error('chrome.' + schema.namespace + ' is not supported on ' +
255 'this platform or manifest version');
256 return undefined;
259 var mod = {};
261 var namespaces = $String.split(schema.namespace, '.');
262 for (var index = 0, name; name = namespaces[index]; index++) {
263 mod[name] = mod[name] || {};
264 mod = mod[name];
267 // Add types to global schemaValidator, the types we depend on from other
268 // namespaces will be added as needed.
269 if (schema.types) {
270 $Array.forEach(schema.types, function(t) {
271 if (!isSchemaNodeSupported(t, platform, manifestVersion))
272 return;
273 schemaUtils.schemaValidator.addTypes(t);
274 }, this);
277 // TODO(cduvall): Take out when all APIs have been converted to features.
278 // Returns whether access to the content of a schema should be denied,
279 // based on the presence of "unprivileged" and whether this is an
280 // extension process (versus e.g. a content script).
281 function isSchemaAccessAllowed(itemSchema) {
282 return (contextType == 'BLESSED_EXTENSION') ||
283 schema.unprivileged ||
284 itemSchema.unprivileged;
287 // Setup Functions.
288 if (schema.functions) {
289 $Array.forEach(schema.functions, function(functionDef) {
290 if (functionDef.name in mod) {
291 throw new Error('Function ' + functionDef.name +
292 ' already defined in ' + schema.namespace);
295 if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) {
296 this.apiFunctions_.registerUnavailable(functionDef.name);
297 return;
300 var apiFunction = {};
301 apiFunction.definition = functionDef;
302 apiFunction.name = schema.namespace + '.' + functionDef.name;
304 if (!GetAvailability(apiFunction.name).is_available ||
305 (checkUnprivileged && !isSchemaAccessAllowed(functionDef))) {
306 this.apiFunctions_.registerUnavailable(functionDef.name);
307 return;
310 // TODO(aa): It would be best to run this in a unit test, but in order
311 // to do that we would need to better factor this code so that it
312 // doesn't depend on so much v8::Extension machinery.
313 if (logging.DCHECK_IS_ON() &&
314 schemaUtils.isFunctionSignatureAmbiguous(apiFunction.definition)) {
315 throw new Error(
316 apiFunction.name + ' has ambiguous optional arguments. ' +
317 'To implement custom disambiguation logic, add ' +
318 '"allowAmbiguousOptionalArguments" to the function\'s schema.');
321 this.apiFunctions_.register(functionDef.name, apiFunction);
323 mod[functionDef.name] = $Function.bind(function() {
324 var args = $Array.slice(arguments);
325 if (this.updateArgumentsPreValidate)
326 args = $Function.apply(this.updateArgumentsPreValidate, this, args);
328 args = schemaUtils.normalizeArgumentsAndValidate(args, this);
329 if (this.updateArgumentsPostValidate) {
330 args = $Function.apply(this.updateArgumentsPostValidate,
331 this,
332 args);
335 sendRequestHandler.clearCalledSendRequest();
337 var retval;
338 if (this.handleRequest) {
339 retval = $Function.apply(this.handleRequest, this, args);
340 } else {
341 var optArgs = {
342 customCallback: this.customCallback
344 retval = sendRequest(this.name, args,
345 this.definition.parameters,
346 optArgs);
348 sendRequestHandler.clearCalledSendRequest();
350 // Validate return value if in sanity check mode.
351 if (logging.DCHECK_IS_ON() && this.definition.returns)
352 schemaUtils.validate([retval], [this.definition.returns]);
353 return retval;
354 }, apiFunction);
355 }, this);
358 // Setup Events
359 if (schema.events) {
360 $Array.forEach(schema.events, function(eventDef) {
361 if (eventDef.name in mod) {
362 throw new Error('Event ' + eventDef.name +
363 ' already defined in ' + schema.namespace);
365 if (!isSchemaNodeSupported(eventDef, platform, manifestVersion))
366 return;
368 var eventName = schema.namespace + "." + eventDef.name;
369 if (!GetAvailability(eventName).is_available ||
370 (checkUnprivileged && !isSchemaAccessAllowed(eventDef))) {
371 return;
374 var options = eventDef.options || {};
375 if (eventDef.filters && eventDef.filters.length > 0)
376 options.supportsFilters = true;
378 var parameters = eventDef.parameters;
379 if (this.customEvent_) {
380 mod[eventDef.name] = new this.customEvent_(
381 eventName, parameters, eventDef.extraParameters, options);
382 } else {
383 mod[eventDef.name] = new Event(eventName, parameters, options);
385 }, this);
388 function addProperties(m, parentDef) {
389 var properties = parentDef.properties;
390 if (!properties)
391 return;
393 forEach(properties, function(propertyName, propertyDef) {
394 if (propertyName in m)
395 return; // TODO(kalman): be strict like functions/events somehow.
396 if (!isSchemaNodeSupported(propertyDef, platform, manifestVersion))
397 return;
398 if (!GetAvailability(schema.namespace + "." +
399 propertyName).is_available ||
400 (checkUnprivileged && !isSchemaAccessAllowed(propertyDef))) {
401 return;
404 var value = propertyDef.value;
405 if (value) {
406 // Values may just have raw types as defined in the JSON, such
407 // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here.
408 // TODO(kalman): enforce that things with a "value" property can't
409 // define their own types.
410 var type = propertyDef.type || typeof(value);
411 if (type === 'integer' || type === 'number') {
412 value = parseInt(value);
413 } else if (type === 'boolean') {
414 value = value === 'true';
415 } else if (propertyDef['$ref']) {
416 var ref = propertyDef['$ref'];
417 var type = utils.loadTypeSchema(propertyDef['$ref'], schema);
418 logging.CHECK(type, 'Schema for $ref type ' + ref + ' not found');
419 var constructor = createCustomType(type);
420 var args = value;
421 // For an object propertyDef, |value| is an array of constructor
422 // arguments, but we want to pass the arguments directly (i.e.
423 // not as an array), so we have to fake calling |new| on the
424 // constructor.
425 value = { __proto__: constructor.prototype };
426 $Function.apply(constructor, value, args);
427 // Recursively add properties.
428 addProperties(value, propertyDef);
429 } else if (type === 'object') {
430 // Recursively add properties.
431 addProperties(value, propertyDef);
432 } else if (type !== 'string') {
433 throw new Error('NOT IMPLEMENTED (extension_api.json error): ' +
434 'Cannot parse values for type "' + type + '"');
436 m[propertyName] = value;
441 addProperties(mod, schema);
443 // This generate() call is considered successful if any functions,
444 // properties, or events were created.
445 var success = ($Object.keys(mod).length > 0);
447 // Special case: webViewRequest is a vacuous API which just copies its
448 // implementation from declarativeWebRequest.
450 // TODO(kalman): This would be unnecessary if we did these checks after the
451 // hooks (i.e. this.runHooks_(mod)). The reason we don't is to be very
452 // conservative with running any JS which might actually be for an API
453 // which isn't available, but this is probably overly cautious given the
454 // C++ is only giving us APIs which are available. FIXME.
455 if (schema.namespace == 'webViewRequest') {
456 success = true;
459 // Special case: runtime.lastError is only occasionally set, so
460 // specifically check its availability.
461 if (schema.namespace == 'runtime' &&
462 GetAvailability('runtime.lastError').is_available) {
463 success = true;
466 if (!success) {
467 var availability = GetAvailability(schema.namespace);
468 // If an API was available it should have been successfully generated.
469 logging.DCHECK(!availability.is_available,
470 schema.namespace + ' was available but not generated');
471 console.error('chrome.' + schema.namespace + ' is not available: ' +
472 availability.message);
473 return;
476 this.runHooks_(mod);
477 return mod;
481 exports.Binding = Binding;