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