Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / extensions / renderer / resources / binding.js
blobcf671cbcab4edb6ce3de4b414e562f76a32c67ba
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       var keepAlivePromise = requireAsync('keep_alive').then(function(module) {
77         return module.createKeepAlive();
78       });
79       $Function.apply(customizedFunction, this, args).then(function(result) {
80         if (callback) {
81           sendRequestHandler.safeCallbackApply(name, {'stack': stack}, callback,
82                                                [result]);
83         }
84       }).catch(function(error) {
85         if (callback) {
86           var message = exceptionHandler.safeErrorToString(error, true);
87           lastError.run(name, message, stack, callback);
88         }
89       }).then(function() {
90         keepAlivePromise.then(function(keepAlive) {
91           keepAlive.close();
92         });
93       });
94     });
97 APIFunctions.prototype.setUpdateArgumentsPostValidate =
98     function(apiName, customizedFunction) {
99   return this.setHook_(
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.
120   var self = this;
121   self.functionSchemas = {};
122   $Array.forEach(schema.functions, function(f) {
123     self.functionSchemas[f.name] = {
124       name: f.name,
125       definition: f
126     }
127   });
130 // Get the platform from navigator.appVersion.
131 function getPlatform() {
132   var platforms = [
133     [/CrOS Touch/, "chromeos touch"],
134     [/CrOS/, "chromeos"],
135     [/Linux/, "linux"],
136     [/Mac/, "mac"],
137     [/Win/, "win"],
138   ];
140   for (var i = 0; i < platforms.length; i++) {
141     if ($RegExp.test(platforms[i][0], navigator.appVersion)) {
142       return platforms[i][1];
143     }
144   }
145   return "unknown";
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 ' +
169                 type.id + '.');
170   var customType = jsModule[jsModuleName];
171   logging.CHECK(customType, jsModuleName + ' must export itself.');
172   customType.prototype = new CustomBindingsObject();
173   customType.prototype.setSchema(type);
174   return customType;
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.
193   //
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;
203   },
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);
211   },
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))
217         return;
219       if (!hook)
220         return;
222       hook({
223         apiFunctions: this.apiFunctions_,
224         schema: this.schema_,
225         compiledApi: api
226       }, extensionId, contextType);
227     }, this);
228   },
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;
237       if (shouldCheck)
238         return shouldCheck;
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)
244               shouldCheck = true;
245           });
246         }
247       });
248       if (shouldCheck)
249         return shouldCheck;
251       for (var property in schema.properties) {
252         if ($Object.hasOwnProperty(schema, property) &&
253             'unprivileged' in schema.properties[property]) {
254           shouldCheck = true;
255           break;
256         }
257       }
258       return shouldCheck;
259     }
260     var checkUnprivileged = shouldCheckUnprivileged();
262     // TODO(kalman/cduvall): Make GetAvailability handle this, then delete the
263     // supporting code.
264     if (!isSchemaNodeSupported(schema, platform, manifestVersion)) {
265       console.error('chrome.' + schema.namespace + ' is not supported on ' +
266                     'this platform or manifest version');
267       return undefined;
268     }
270     var mod = {};
272     var namespaces = $String.split(schema.namespace, '.');
273     for (var index = 0, name; name = namespaces[index]; index++) {
274       mod[name] = mod[name] || {};
275       mod = mod[name];
276     }
278     if (schema.types) {
279       $Array.forEach(schema.types, function(t) {
280         if (!isSchemaNodeSupported(t, platform, manifestVersion))
281           return;
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'];
289         if (enumValues) {
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) ==
293                              schema.namespace);
294           // Note: + 1 because it ends in a '.', e.g., 'fooApi.Type'.
295           var id = $String.substr(t.id, schema.namespace.length + 1);
296           mod[id] = {};
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:
306               var propertyName =
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
312               // underscore.
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;
317             }
318           });
319         }
320       }, this);
321     }
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;
331     };
333     // Setup Functions.
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);
339         }
341         if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) {
342           this.apiFunctions_.registerUnavailable(functionDef.name);
343           return;
344         }
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);
353           return;
354         }
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)) {
361           throw new Error(
362               apiFunction.name + ' has ambiguous optional arguments. ' +
363               'To implement custom disambiguation logic, add ' +
364               '"allowAmbiguousOptionalArguments" to the function\'s schema.');
365         }
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,
377                                    this,
378                                    args);
379           }
381           sendRequestHandler.clearCalledSendRequest();
383           var retval;
384           if (this.handleRequest) {
385             retval = $Function.apply(this.handleRequest, this, args);
386           } else {
387             var optArgs = {
388               customCallback: this.customCallback
389             };
390             retval = sendRequest(this.name, args,
391                                  this.definition.parameters,
392                                  optArgs);
393           }
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]);
399           return retval;
400         }, apiFunction);
401       }, this);
402     }
404     // Setup Events
405     if (schema.events) {
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);
410         }
411         if (!isSchemaNodeSupported(eventDef, platform, manifestVersion))
412           return;
414         var eventName = schema.namespace + "." + eventDef.name;
415         if (!GetAvailability(eventName).is_available ||
416             (checkUnprivileged && !isSchemaAccessAllowed(eventDef))) {
417           return;
418         }
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);
428         } else {
429           mod[eventDef.name] = new Event(eventName, parameters, options);
430         }
431       }, this);
432     }
434     function addProperties(m, parentDef) {
435       var properties = parentDef.properties;
436       if (!properties)
437         return;
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))
443           return;
444         if (!GetAvailability(schema.namespace + "." +
445               propertyName).is_available ||
446             (checkUnprivileged && !isSchemaAccessAllowed(propertyDef))) {
447           return;
448         }
450         var value = propertyDef.value;
451         if (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);
466             var args = value;
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
470             // constructor.
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 + '"');
481           }
482           m[propertyName] = value;
483         }
484       });
485     };
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.
495     //
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') {
502       success = true;
503     }
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) {
509       success = true;
510     }
512     if (!success) {
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);
519       return;
520     }
522     this.runHooks_(mod);
523     return mod;
524   }
527 exports.Binding = Binding;