Home | History | Annotate | Download | only in resources
      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 
      5 var Event = require('event_bindings').Event;
      6 var forEach = require('utils').forEach;
      7 var GetAvailability = requireNative('v8_context').GetAvailability;
      8 var logActivity = requireNative('activityLogger');
      9 var logging = requireNative('logging');
     10 var process = requireNative('process');
     11 var schemaRegistry = requireNative('schema_registry');
     12 var schemaUtils = require('schemaUtils');
     13 var utils = require('utils');
     14 var sendRequestHandler = require('sendRequest');
     15 
     16 var contextType = process.GetContextType();
     17 var extensionId = process.GetExtensionId();
     18 var manifestVersion = process.GetManifestVersion();
     19 var sendRequest = sendRequestHandler.sendRequest;
     20 
     21 // Stores the name and definition of each API function, with methods to
     22 // modify their behaviour (such as a custom way to handle requests to the
     23 // API, a custom callback, etc).
     24 function APIFunctions(namespace) {
     25   this.apiFunctions_ = {};
     26   this.unavailableApiFunctions_ = {};
     27   this.namespace = namespace;
     28 }
     29 
     30 APIFunctions.prototype.register = function(apiName, apiFunction) {
     31   this.apiFunctions_[apiName] = apiFunction;
     32 };
     33 
     34 // Registers a function as existing but not available, meaning that calls to
     35 // the set* methods that reference this function should be ignored rather
     36 // than throwing Errors.
     37 APIFunctions.prototype.registerUnavailable = function(apiName) {
     38   this.unavailableApiFunctions_[apiName] = apiName;
     39 };
     40 
     41 APIFunctions.prototype.setHook_ =
     42     function(apiName, propertyName, customizedFunction) {
     43   if ($Object.hasOwnProperty(this.unavailableApiFunctions_, apiName))
     44     return;
     45   if (!$Object.hasOwnProperty(this.apiFunctions_, apiName))
     46     throw new Error('Tried to set hook for unknown API "' + apiName + '"');
     47   this.apiFunctions_[apiName][propertyName] = customizedFunction;
     48 };
     49 
     50 APIFunctions.prototype.setHandleRequest =
     51     function(apiName, customizedFunction) {
     52   var prefix = this.namespace;
     53   return this.setHook_(apiName, 'handleRequest',
     54     function() {
     55       var ret = $Function.apply(customizedFunction, this, arguments);
     56       // Logs API calls to the Activity Log if it doesn't go through an
     57       // ExtensionFunction.
     58       if (!sendRequestHandler.getCalledSendRequest())
     59         logActivity.LogAPICall(extensionId, prefix + "." + apiName,
     60             $Array.slice(arguments));
     61       return ret;
     62     });
     63 };
     64 
     65 APIFunctions.prototype.setUpdateArgumentsPostValidate =
     66     function(apiName, customizedFunction) {
     67   return this.setHook_(
     68     apiName, 'updateArgumentsPostValidate', customizedFunction);
     69 };
     70 
     71 APIFunctions.prototype.setUpdateArgumentsPreValidate =
     72     function(apiName, customizedFunction) {
     73   return this.setHook_(
     74     apiName, 'updateArgumentsPreValidate', customizedFunction);
     75 };
     76 
     77 APIFunctions.prototype.setCustomCallback =
     78     function(apiName, customizedFunction) {
     79   return this.setHook_(apiName, 'customCallback', customizedFunction);
     80 };
     81 
     82 function CustomBindingsObject() {
     83 }
     84 
     85 CustomBindingsObject.prototype.setSchema = function(schema) {
     86   // The functions in the schema are in list form, so we move them into a
     87   // dictionary for easier access.
     88   var self = this;
     89   self.functionSchemas = {};
     90   $Array.forEach(schema.functions, function(f) {
     91     self.functionSchemas[f.name] = {
     92       name: f.name,
     93       definition: f
     94     }
     95   });
     96 };
     97 
     98 // Get the platform from navigator.appVersion.
     99 function getPlatform() {
    100   var platforms = [
    101     [/CrOS Touch/, "chromeos touch"],
    102     [/CrOS/, "chromeos"],
    103     [/Linux/, "linux"],
    104     [/Mac/, "mac"],
    105     [/Win/, "win"],
    106   ];
    107 
    108   for (var i = 0; i < platforms.length; i++) {
    109     if ($RegExp.test(platforms[i][0], navigator.appVersion)) {
    110       return platforms[i][1];
    111     }
    112   }
    113   return "unknown";
    114 }
    115 
    116 function isPlatformSupported(schemaNode, platform) {
    117   return !schemaNode.platforms ||
    118       schemaNode.platforms.indexOf(platform) > -1;
    119 }
    120 
    121 function isManifestVersionSupported(schemaNode, manifestVersion) {
    122   return !schemaNode.maximumManifestVersion ||
    123       manifestVersion <= schemaNode.maximumManifestVersion;
    124 }
    125 
    126 function isSchemaNodeSupported(schemaNode, platform, manifestVersion) {
    127   return isPlatformSupported(schemaNode, platform) &&
    128       isManifestVersionSupported(schemaNode, manifestVersion);
    129 }
    130 
    131 function createCustomType(type) {
    132   var jsModuleName = type.js_module;
    133   logging.CHECK(jsModuleName, 'Custom type ' + type.id +
    134                 ' has no "js_module" property.');
    135   var jsModule = require(jsModuleName);
    136   logging.CHECK(jsModule, 'No module ' + jsModuleName + ' found for ' +
    137                 type.id + '.');
    138   var customType = jsModule[jsModuleName];
    139   logging.CHECK(customType, jsModuleName + ' must export itself.');
    140   customType.prototype = new CustomBindingsObject();
    141   customType.prototype.setSchema(type);
    142   return customType;
    143 }
    144 
    145 var platform = getPlatform();
    146 
    147 function Binding(schema) {
    148   this.schema_ = schema;
    149   this.apiFunctions_ = new APIFunctions(schema.namespace);
    150   this.customEvent_ = null;
    151   this.customHooks_ = [];
    152 };
    153 
    154 Binding.create = function(apiName) {
    155   return new Binding(schemaRegistry.GetSchema(apiName));
    156 };
    157 
    158 Binding.prototype = {
    159   // The API through which the ${api_name}_custom_bindings.js files customize
    160   // their API bindings beyond what can be generated.
    161   //
    162   // There are 2 types of customizations available: those which are required in
    163   // order to do the schema generation (registerCustomEvent and
    164   // registerCustomType), and those which can only run after the bindings have
    165   // been generated (registerCustomHook).
    166 
    167   // Registers a custom event type for the API identified by |namespace|.
    168   // |event| is the event's constructor.
    169   registerCustomEvent: function(event) {
    170     this.customEvent_ = event;
    171   },
    172 
    173   // Registers a function |hook| to run after the schema for all APIs has been
    174   // generated.  The hook is passed as its first argument an "API" object to
    175   // interact with, and second the current extension ID. See where
    176   // |customHooks| is used.
    177   registerCustomHook: function(fn) {
    178     $Array.push(this.customHooks_, fn);
    179   },
    180 
    181   // TODO(kalman/cduvall): Refactor this so |runHooks_| is not needed.
    182   runHooks_: function(api) {
    183     $Array.forEach(this.customHooks_, function(hook) {
    184       if (!isSchemaNodeSupported(this.schema_, platform, manifestVersion))
    185         return;
    186 
    187       if (!hook)
    188         return;
    189 
    190       hook({
    191         apiFunctions: this.apiFunctions_,
    192         schema: this.schema_,
    193         compiledApi: api
    194       }, extensionId, contextType);
    195     }, this);
    196   },
    197 
    198   // Generates the bindings from |this.schema_| and integrates any custom
    199   // bindings that might be present.
    200   generate: function() {
    201     var schema = this.schema_;
    202 
    203     function shouldCheckUnprivileged() {
    204       var shouldCheck = 'unprivileged' in schema;
    205       if (shouldCheck)
    206         return shouldCheck;
    207 
    208       $Array.forEach(['functions', 'events'], function(type) {
    209         if ($Object.hasOwnProperty(schema, type)) {
    210           $Array.forEach(schema[type], function(node) {
    211             if ('unprivileged' in node)
    212               shouldCheck = true;
    213           });
    214         }
    215       });
    216       if (shouldCheck)
    217         return shouldCheck;
    218 
    219       for (var property in schema.properties) {
    220         if ($Object.hasOwnProperty(schema, property) &&
    221             'unprivileged' in schema.properties[property]) {
    222           shouldCheck = true;
    223           break;
    224         }
    225       }
    226       return shouldCheck;
    227     }
    228     var checkUnprivileged = shouldCheckUnprivileged();
    229 
    230     // TODO(kalman/cduvall): Make GetAvailability handle this, then delete the
    231     // supporting code.
    232     if (!isSchemaNodeSupported(schema, platform, manifestVersion)) {
    233       console.error('chrome.' + schema.namespace + ' is not supported on ' +
    234                     'this platform or manifest version');
    235       return undefined;
    236     }
    237 
    238     var mod = {};
    239 
    240     var namespaces = $String.split(schema.namespace, '.');
    241     for (var index = 0, name; name = namespaces[index]; index++) {
    242       mod[name] = mod[name] || {};
    243       mod = mod[name];
    244     }
    245 
    246     // Add types to global schemaValidator, the types we depend on from other
    247     // namespaces will be added as needed.
    248     if (schema.types) {
    249       $Array.forEach(schema.types, function(t) {
    250         if (!isSchemaNodeSupported(t, platform, manifestVersion))
    251           return;
    252         schemaUtils.schemaValidator.addTypes(t);
    253       }, this);
    254     }
    255 
    256     // TODO(cduvall): Take out when all APIs have been converted to features.
    257     // Returns whether access to the content of a schema should be denied,
    258     // based on the presence of "unprivileged" and whether this is an
    259     // extension process (versus e.g. a content script).
    260     function isSchemaAccessAllowed(itemSchema) {
    261       return (contextType == 'BLESSED_EXTENSION') ||
    262              schema.unprivileged ||
    263              itemSchema.unprivileged;
    264     };
    265 
    266     // Setup Functions.
    267     if (schema.functions) {
    268       $Array.forEach(schema.functions, function(functionDef) {
    269         if (functionDef.name in mod) {
    270           throw new Error('Function ' + functionDef.name +
    271                           ' already defined in ' + schema.namespace);
    272         }
    273 
    274         if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) {
    275           this.apiFunctions_.registerUnavailable(functionDef.name);
    276           return;
    277         }
    278 
    279         var apiFunction = {};
    280         apiFunction.definition = functionDef;
    281         apiFunction.name = schema.namespace + '.' + functionDef.name;
    282 
    283         if (!GetAvailability(apiFunction.name).is_available ||
    284             (checkUnprivileged && !isSchemaAccessAllowed(functionDef))) {
    285           this.apiFunctions_.registerUnavailable(functionDef.name);
    286           return;
    287         }
    288 
    289         // TODO(aa): It would be best to run this in a unit test, but in order
    290         // to do that we would need to better factor this code so that it
    291         // doesn't depend on so much v8::Extension machinery.
    292         if (logging.DCHECK_IS_ON() &&
    293             schemaUtils.isFunctionSignatureAmbiguous(apiFunction.definition)) {
    294           throw new Error(
    295               apiFunction.name + ' has ambiguous optional arguments. ' +
    296               'To implement custom disambiguation logic, add ' +
    297               '"allowAmbiguousOptionalArguments" to the function\'s schema.');
    298         }
    299 
    300         this.apiFunctions_.register(functionDef.name, apiFunction);
    301 
    302         mod[functionDef.name] = $Function.bind(function() {
    303           var args = $Array.slice(arguments);
    304           if (this.updateArgumentsPreValidate)
    305             args = $Function.apply(this.updateArgumentsPreValidate, this, args);
    306 
    307           args = schemaUtils.normalizeArgumentsAndValidate(args, this);
    308           if (this.updateArgumentsPostValidate) {
    309             args = $Function.apply(this.updateArgumentsPostValidate,
    310                                    this,
    311                                    args);
    312           }
    313 
    314           sendRequestHandler.clearCalledSendRequest();
    315 
    316           var retval;
    317           if (this.handleRequest) {
    318             retval = $Function.apply(this.handleRequest, this, args);
    319           } else {
    320             var optArgs = {
    321               customCallback: this.customCallback
    322             };
    323             retval = sendRequest(this.name, args,
    324                                  this.definition.parameters,
    325                                  optArgs);
    326           }
    327           sendRequestHandler.clearCalledSendRequest();
    328 
    329           // Validate return value if in sanity check mode.
    330           if (logging.DCHECK_IS_ON() && this.definition.returns)
    331             schemaUtils.validate([retval], [this.definition.returns]);
    332           return retval;
    333         }, apiFunction);
    334       }, this);
    335     }
    336 
    337     // Setup Events
    338     if (schema.events) {
    339       $Array.forEach(schema.events, function(eventDef) {
    340         if (eventDef.name in mod) {
    341           throw new Error('Event ' + eventDef.name +
    342                           ' already defined in ' + schema.namespace);
    343         }
    344         if (!isSchemaNodeSupported(eventDef, platform, manifestVersion))
    345           return;
    346 
    347         var eventName = schema.namespace + "." + eventDef.name;
    348         if (!GetAvailability(eventName).is_available ||
    349             (checkUnprivileged && !isSchemaAccessAllowed(eventDef))) {
    350           return;
    351         }
    352 
    353         var options = eventDef.options || {};
    354         if (eventDef.filters && eventDef.filters.length > 0)
    355           options.supportsFilters = true;
    356 
    357         var parameters = eventDef.parameters;
    358         if (this.customEvent_) {
    359           mod[eventDef.name] = new this.customEvent_(
    360               eventName, parameters, eventDef.extraParameters, options);
    361         } else {
    362           mod[eventDef.name] = new Event(eventName, parameters, options);
    363         }
    364       }, this);
    365     }
    366 
    367     function addProperties(m, parentDef) {
    368       var properties = parentDef.properties;
    369       if (!properties)
    370         return;
    371 
    372       forEach(properties, function(propertyName, propertyDef) {
    373         if (propertyName in m)
    374           return;  // TODO(kalman): be strict like functions/events somehow.
    375         if (!isSchemaNodeSupported(propertyDef, platform, manifestVersion))
    376           return;
    377         if (!GetAvailability(schema.namespace + "." +
    378               propertyName).is_available ||
    379             (checkUnprivileged && !isSchemaAccessAllowed(propertyDef))) {
    380           return;
    381         }
    382 
    383         var value = propertyDef.value;
    384         if (value) {
    385           // Values may just have raw types as defined in the JSON, such
    386           // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here.
    387           // TODO(kalman): enforce that things with a "value" property can't
    388           // define their own types.
    389           var type = propertyDef.type || typeof(value);
    390           if (type === 'integer' || type === 'number') {
    391             value = parseInt(value);
    392           } else if (type === 'boolean') {
    393             value = value === 'true';
    394           } else if (propertyDef['$ref']) {
    395             var ref = propertyDef['$ref'];
    396             var type = utils.loadTypeSchema(propertyDef['$ref'], schema);
    397             logging.CHECK(type, 'Schema for $ref type ' + ref + ' not found');
    398             var constructor = createCustomType(type);
    399             var args = value;
    400             // For an object propertyDef, |value| is an array of constructor
    401             // arguments, but we want to pass the arguments directly (i.e.
    402             // not as an array), so we have to fake calling |new| on the
    403             // constructor.
    404             value = { __proto__: constructor.prototype };
    405             $Function.apply(constructor, value, args);
    406             // Recursively add properties.
    407             addProperties(value, propertyDef);
    408           } else if (type === 'object') {
    409             // Recursively add properties.
    410             addProperties(value, propertyDef);
    411           } else if (type !== 'string') {
    412             throw new Error('NOT IMPLEMENTED (extension_api.json error): ' +
    413                 'Cannot parse values for type "' + type + '"');
    414           }
    415           m[propertyName] = value;
    416         }
    417       });
    418     };
    419 
    420     addProperties(mod, schema);
    421 
    422     var availability = GetAvailability(schema.namespace);
    423     if (!availability.is_available && $Object.keys(mod).length == 0) {
    424       console.error('chrome.' + schema.namespace + ' is not available: ' +
    425                         availability.message);
    426       return;
    427     }
    428 
    429     this.runHooks_(mod);
    430     return mod;
    431   }
    432 };
    433 
    434 exports.Binding = Binding;
    435