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.
4
5var Event = require('event_bindings').Event;
6var forEach = require('utils').forEach;
7var GetAvailability = requireNative('v8_context').GetAvailability;
8var exceptionHandler = require('uncaught_exception_handler');
9var lastError = require('lastError');
10var logActivity = requireNative('activityLogger');
11var logging = requireNative('logging');
12var process = requireNative('process');
13var schemaRegistry = requireNative('schema_registry');
14var schemaUtils = require('schemaUtils');
15var utils = require('utils');
16var sendRequestHandler = require('sendRequest');
17
18var contextType = process.GetContextType();
19var extensionId = process.GetExtensionId();
20var manifestVersion = process.GetManifestVersion();
21var sendRequest = sendRequestHandler.sendRequest;
22
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).
26function APIFunctions(namespace) {
27  this.apiFunctions_ = {};
28  this.unavailableApiFunctions_ = {};
29  this.namespace = namespace;
30}
31
32APIFunctions.prototype.register = function(apiName, apiFunction) {
33  this.apiFunctions_[apiName] = apiFunction;
34};
35
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.
39APIFunctions.prototype.registerUnavailable = function(apiName) {
40  this.unavailableApiFunctions_[apiName] = apiName;
41};
42
43APIFunctions.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;
50};
51
52APIFunctions.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    });
65};
66
67APIFunctions.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    });
84};
85
86APIFunctions.prototype.setUpdateArgumentsPostValidate =
87    function(apiName, customizedFunction) {
88  return this.setHook_(
89    apiName, 'updateArgumentsPostValidate', customizedFunction);
90};
91
92APIFunctions.prototype.setUpdateArgumentsPreValidate =
93    function(apiName, customizedFunction) {
94  return this.setHook_(
95    apiName, 'updateArgumentsPreValidate', customizedFunction);
96};
97
98APIFunctions.prototype.setCustomCallback =
99    function(apiName, customizedFunction) {
100  return this.setHook_(apiName, 'customCallback', customizedFunction);
101};
102
103function CustomBindingsObject() {
104}
105
106CustomBindingsObject.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
115    }
116  });
117};
118
119// Get the platform from navigator.appVersion.
120function getPlatform() {
121  var platforms = [
122    [/CrOS Touch/, "chromeos touch"],
123    [/CrOS/, "chromeos"],
124    [/Linux/, "linux"],
125    [/Mac/, "mac"],
126    [/Win/, "win"],
127  ];
128
129  for (var i = 0; i < platforms.length; i++) {
130    if ($RegExp.test(platforms[i][0], navigator.appVersion)) {
131      return platforms[i][1];
132    }
133  }
134  return "unknown";
135}
136
137function isPlatformSupported(schemaNode, platform) {
138  return !schemaNode.platforms ||
139      $Array.indexOf(schemaNode.platforms, platform) > -1;
140}
141
142function isManifestVersionSupported(schemaNode, manifestVersion) {
143  return !schemaNode.maximumManifestVersion ||
144      manifestVersion <= schemaNode.maximumManifestVersion;
145}
146
147function isSchemaNodeSupported(schemaNode, platform, manifestVersion) {
148  return isPlatformSupported(schemaNode, platform) &&
149      isManifestVersionSupported(schemaNode, manifestVersion);
150}
151
152function 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;
164}
165
166var platform = getPlatform();
167
168function Binding(schema) {
169  this.schema_ = schema;
170  this.apiFunctions_ = new APIFunctions(schema.namespace);
171  this.customEvent_ = null;
172  this.customHooks_ = [];
173};
174
175Binding.create = function(apiName) {
176  return new Binding(schemaRegistry.GetSchema(apiName));
177};
178
179Binding.prototype = {
180  // The API through which the ${api_name}_custom_bindings.js files customize
181  // their API bindings beyond what can be generated.
182  //
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).
187
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;
192  },
193
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);
200  },
201
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;
207
208      if (!hook)
209        return;
210
211      hook({
212        apiFunctions: this.apiFunctions_,
213        schema: this.schema_,
214        compiledApi: api
215      }, extensionId, contextType);
216    }, this);
217  },
218
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_;
223
224    function shouldCheckUnprivileged() {
225      var shouldCheck = 'unprivileged' in schema;
226      if (shouldCheck)
227        return shouldCheck;
228
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;
234          });
235        }
236      });
237      if (shouldCheck)
238        return shouldCheck;
239
240      for (var property in schema.properties) {
241        if ($Object.hasOwnProperty(schema, property) &&
242            'unprivileged' in schema.properties[property]) {
243          shouldCheck = true;
244          break;
245        }
246      }
247      return shouldCheck;
248    }
249    var checkUnprivileged = shouldCheckUnprivileged();
250
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;
257    }
258
259    var mod = {};
260
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];
265    }
266
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);
275    }
276
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;
285    };
286
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);
293        }
294
295        if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) {
296          this.apiFunctions_.registerUnavailable(functionDef.name);
297          return;
298        }
299
300        var apiFunction = {};
301        apiFunction.definition = functionDef;
302        apiFunction.name = schema.namespace + '.' + functionDef.name;
303
304        if (!GetAvailability(apiFunction.name).is_available ||
305            (checkUnprivileged && !isSchemaAccessAllowed(functionDef))) {
306          this.apiFunctions_.registerUnavailable(functionDef.name);
307          return;
308        }
309
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.');
319        }
320
321        this.apiFunctions_.register(functionDef.name, apiFunction);
322
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);
327
328          args = schemaUtils.normalizeArgumentsAndValidate(args, this);
329          if (this.updateArgumentsPostValidate) {
330            args = $Function.apply(this.updateArgumentsPostValidate,
331                                   this,
332                                   args);
333          }
334
335          sendRequestHandler.clearCalledSendRequest();
336
337          var retval;
338          if (this.handleRequest) {
339            retval = $Function.apply(this.handleRequest, this, args);
340          } else {
341            var optArgs = {
342              customCallback: this.customCallback
343            };
344            retval = sendRequest(this.name, args,
345                                 this.definition.parameters,
346                                 optArgs);
347          }
348          sendRequestHandler.clearCalledSendRequest();
349
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);
356    }
357
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);
364        }
365        if (!isSchemaNodeSupported(eventDef, platform, manifestVersion))
366          return;
367
368        var eventName = schema.namespace + "." + eventDef.name;
369        if (!GetAvailability(eventName).is_available ||
370            (checkUnprivileged && !isSchemaAccessAllowed(eventDef))) {
371          return;
372        }
373
374        var options = eventDef.options || {};
375        if (eventDef.filters && eventDef.filters.length > 0)
376          options.supportsFilters = true;
377
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);
384        }
385      }, this);
386    }
387
388    function addProperties(m, parentDef) {
389      var properties = parentDef.properties;
390      if (!properties)
391        return;
392
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;
402        }
403
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 + '"');
435          }
436          m[propertyName] = value;
437        }
438      });
439    };
440
441    addProperties(mod, schema);
442
443    // This generate() call is considered successful if any functions,
444    // properties, or events were created.
445    var success = ($Object.keys(mod).length > 0);
446
447    // Special case: webViewRequest is a vacuous API which just copies its
448    // implementation from declarativeWebRequest.
449    //
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;
457    }
458
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;
464    }
465
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;
474    }
475
476    this.runHooks_(mod);
477    return mod;
478  }
479};
480
481exports.Binding = Binding;
482