Index: chrome/renderer/resources/extensions/schema_binding_generator.js |
diff --git a/chrome/renderer/resources/extensions/schema_binding_generator.js b/chrome/renderer/resources/extensions/schema_binding_generator.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..39d4fd0187507d604e6d0c5d347dade0adedd82d |
--- /dev/null |
+++ b/chrome/renderer/resources/extensions/schema_binding_generator.js |
@@ -0,0 +1,438 @@ |
+// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+require('json_schema'); |
+var schemaRegistry = requireNative('schema_registry'); |
+var sendRequest = require('sendRequest').sendRequest; |
+var utils = require('utils'); |
+var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); |
+var chrome = requireNative('chrome').GetChrome(); |
+var schemaUtils = require('schemaUtils'); |
+var process = requireNative('process'); |
+var manifestVersion = process.GetManifestVersion(); |
+var extensionId = process.GetExtensionId(); |
+var contextType = process.GetContextType(); |
+ |
+// TODO(cduvall): take out. |
+var DCHECK = requireNative('logging').DCHECK; |
+ |
+// The object to generate the bindings for "internal" APIs in, so that |
+// extensions can't directly call them (without access to chromeHidden), |
+// but are still needed for internal mechanisms of extensions (e.g. events). |
+// |
+// This is distinct to the "*Private" APIs which are controlled via |
+// having strict permissions and aren't generated *anywhere* unless needed. |
+// TODO(cduvall): Refactor so internalAPIs isn't necessary. |
+var internalAPIs = {}; |
+chromeHidden.internalAPIs = internalAPIs; |
+ |
+// Stores the name and definition of each API function, with methods to |
+// modify their behaviour (such as a custom way to handle requests to the |
+// API, a custom callback, etc). |
+function APIFunctions() { |
+ this._apiFunctions = {}; |
+ this._unavailableApiFunctions = {}; |
+} |
+APIFunctions.prototype.register = function(apiName, apiFunction) { |
+ this._apiFunctions[apiName] = apiFunction; |
+}; |
+// Registers a function as existing but not available, meaning that calls to |
+// the set* methods that reference this function should be ignored rather |
+// than throwing Errors. |
+APIFunctions.prototype.registerUnavailable = function(apiName) { |
+ this._unavailableApiFunctions[apiName] = apiName; |
+}; |
+APIFunctions.prototype._setHook = |
+ function(apiName, propertyName, customizedFunction) { |
+ if (this._unavailableApiFunctions.hasOwnProperty(apiName)) |
+ return; |
+ if (!this._apiFunctions.hasOwnProperty(apiName)) |
+ throw new Error('Tried to set hook for unknown API "' + apiName + '"'); |
+ this._apiFunctions[apiName][propertyName] = customizedFunction; |
+}; |
+APIFunctions.prototype.setHandleRequest = |
+ function(apiName, customizedFunction) { |
+ return this._setHook(apiName, 'handleRequest', customizedFunction); |
+}; |
+APIFunctions.prototype.setUpdateArgumentsPostValidate = |
+ function(apiName, customizedFunction) { |
+ return this._setHook( |
+ apiName, 'updateArgumentsPostValidate', customizedFunction); |
+}; |
+APIFunctions.prototype.setUpdateArgumentsPreValidate = |
+ function(apiName, customizedFunction) { |
+ return this._setHook( |
+ apiName, 'updateArgumentsPreValidate', customizedFunction); |
+}; |
+APIFunctions.prototype.setCustomCallback = |
+ function(apiName, customizedFunction) { |
+ return this._setHook(apiName, 'customCallback', customizedFunction); |
+}; |
+ |
+var apiFunctions = new APIFunctions(); |
not at google - send to devlin
2013/01/25 00:44:31
ok, so this shouldn't be global anymore, it should
cduvall
2013/02/12 02:13:47
Done.
|
+ |
+// Wraps the calls to the set* methods of APIFunctions with the namespace of |
+// an API, and validates that all calls to set* methods aren't prefixed with |
+// a namespace. |
+// |
+// For example, if constructed with 'browserAction', a call to |
+// handleRequest('foo') will be transformed into |
+// handleRequest('browserAction.foo'). |
+// |
+// Likewise, if a call to handleRequest is called with 'browserAction.foo', |
+// it will throw an error. |
+// |
+// These help with isolating custom bindings from each other. |
+function NamespacedAPIFunctions(namespace, delegate) { |
+ var self = this; |
+ function wrap(methodName) { |
+ self[methodName] = function(apiName, customizedFunction) { |
+ var prefix = namespace + '.'; |
+ if (apiName.indexOf(prefix) === 0) { |
+ throw new Error(methodName + ' called with "' + apiName + |
+ '" which has a "' + prefix + '" prefix. ' + |
+ 'This is unnecessary and must be left out.'); |
+ } |
+ return delegate[methodName].call(delegate, |
+ prefix + apiName, customizedFunction); |
+ }; |
+ } |
+ |
+ wrap('contains'); |
+ wrap('setHandleRequest'); |
+ wrap('setUpdateArgumentsPostValidate'); |
+ wrap('setUpdateArgumentsPreValidate'); |
+ wrap('setCustomCallback'); |
+} |
+ |
+function CustomBindingsObject() { |
+} |
+CustomBindingsObject.prototype.setSchema = function(schema) { |
+ // The functions in the schema are in list form, so we move them into a |
+ // dictionary for easier access. |
+ var self = this; |
+ self.functionSchemas = {}; |
+ schema.functions.forEach(function(f) { |
+ self.functionSchemas[f.name] = { |
+ name: f.name, |
+ definition: f |
+ } |
+ }); |
+}; |
+ |
+// Get the platform from navigator.appVersion. |
+function getPlatform() { |
+ var platforms = [ |
+ [/CrOS Touch/, "chromeos touch"], |
+ [/CrOS/, "chromeos"], |
+ [/Linux/, "linux"], |
+ [/Mac/, "mac"], |
+ [/Win/, "win"], |
+ ]; |
+ |
+ for (var i = 0; i < platforms.length; i++) { |
+ if (platforms[i][0].test(navigator.appVersion)) { |
+ return platforms[i][1]; |
+ } |
+ } |
+ return "unknown"; |
+} |
+ |
+function isPlatformSupported(schemaNode, platform) { |
+ return !schemaNode.platforms || |
+ schemaNode.platforms.indexOf(platform) > -1; |
+} |
+ |
+function isManifestVersionSupported(schemaNode, manifestVersion) { |
+ return !schemaNode.maximumManifestVersion || |
+ manifestVersion <= schemaNode.maximumManifestVersion; |
+} |
+ |
+function isSchemaNodeSupported(schemaNode, platform, manifestVersion) { |
+ return isPlatformSupported(schemaNode, platform) && |
+ isManifestVersionSupported(schemaNode, manifestVersion); |
+} |
+ |
+var platform = getPlatform(); |
+ |
+function generate(apiDef, eventType, types) { |
+ var customEvent = eventType || null; |
+ var customTypes = types || {}; |
+ |
+ if (!isSchemaNodeSupported(apiDef, platform, manifestVersion)) |
+ return; |
+ |
not at google - send to devlin
2013/01/25 00:44:31
We could and should be way cool here. Firstly: thi
|
+ // See comment on internalAPIs at the top. |
+ var mod = apiDef.internal ? internalAPIs : {}; |
+ |
+ var namespaces = apiDef.namespace.split('.'); |
+ for (var index = 0, name; name = namespaces[index]; index++) { |
+ mod[name] = mod[name] || {}; |
+ mod = mod[name]; |
+ } |
+ |
+ // Add types to global schemaValidator |
+ if (apiDef.types) { |
+ apiDef.types.forEach(function(t) { |
+ if (!isSchemaNodeSupported(t, platform, manifestVersion)) |
+ return; |
+ |
+ schemaUtils.schemaValidator.addTypes(t); |
+ if (t.type == 'object' && customTypes[t.id]) { |
+ var parts = t.id.split("."); |
+ customTypes[t.id].prototype.setSchema(t); |
+ mod[parts[parts.length - 1]] = customTypes[t.id]; |
+ } |
+ }); |
+ } |
+ |
+ // Returns whether access to the content of a schema should be denied, |
+ // based on the presence of "unprivileged" and whether this is an |
+ // extension process (versus e.g. a content script). |
+ function isSchemaAccessAllowed(itemSchema) { |
+ return (contextType == 'BLESSED_EXTENSION') || |
+ apiDef.unprivileged || |
+ itemSchema.unprivileged; |
+ } |
+ |
+ // Adds a getter that throws an access denied error to object |mod| |
+ // for property |name|. |
+ function addUnprivilegedAccessGetter(mod, name) { |
+ mod.__defineGetter__(name, function() { |
+ throw new Error( |
+ '"' + name + '" can only be used in extension processes. See ' + |
+ 'the content scripts documentation for more details.'); |
+ }); |
+ } |
+ |
+ // Setup Functions. |
+ if (apiDef.functions) { |
+ apiDef.functions.forEach(function(functionDef) { |
+ if (functionDef.name in mod) { |
+ throw new Error('Function ' + functionDef.name + |
+ ' already defined in ' + apiDef.namespace); |
+ } |
+ |
+ var apiFunctionName = apiDef.namespace + "." + functionDef.name; |
+ |
+ if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) { |
+ apiFunctions.registerUnavailable(apiFunctionName); |
+ return; |
+ } |
+ if (!isSchemaAccessAllowed(functionDef)) { |
+ apiFunctions.registerUnavailable(apiFunctionName); |
+ addUnprivilegedAccessGetter(mod, functionDef.name); |
+ return; |
+ } |
+ |
+ var apiFunction = {}; |
+ apiFunction.definition = functionDef; |
+ apiFunction.name = apiFunctionName; |
+ |
+ // TODO(aa): It would be best to run this in a unit test, but in order |
+ // to do that we would need to better factor this code so that it |
+ // doesn't depend on so much v8::Extension machinery. |
+ if (chromeHidden.validateAPI && |
+ schemaUtils.isFunctionSignatureAmbiguous( |
+ apiFunction.definition)) { |
+ throw new Error( |
+ apiFunction.name + ' has ambiguous optional arguments. ' + |
+ 'To implement custom disambiguation logic, add ' + |
+ '"allowAmbiguousOptionalArguments" to the function\'s schema.'); |
+ } |
+ |
+ apiFunctions.register(apiFunction.name, apiFunction); |
+ |
+ mod[functionDef.name] = (function() { |
+ var args = Array.prototype.slice.call(arguments); |
+ if (this.updateArgumentsPreValidate) |
+ args = this.updateArgumentsPreValidate.apply(this, args); |
+ |
+ args = schemaUtils.normalizeArgumentsAndValidate(args, this); |
+ if (this.updateArgumentsPostValidate) |
+ args = this.updateArgumentsPostValidate.apply(this, args); |
+ |
+ var retval; |
+ if (this.handleRequest) { |
+ retval = this.handleRequest.apply(this, args); |
+ } else { |
+ var optArgs = { |
+ customCallback: this.customCallback |
+ }; |
+ retval = sendRequest(this.name, args, |
+ this.definition.parameters, |
+ optArgs); |
+ } |
+ |
+ // Validate return value if defined - only in debug. |
+ if (chromeHidden.validateCallbacks && |
+ this.definition.returns) { |
+ schemaUtils.validate([retval], [this.definition.returns]); |
+ } |
+ return retval; |
+ }).bind(apiFunction); |
+ }); |
+ } |
+ |
+ // Setup Events |
+ if (apiDef.events) { |
+ apiDef.events.forEach(function(eventDef) { |
+ if (eventDef.name in mod) { |
+ throw new Error('Event ' + eventDef.name + |
+ ' already defined in ' + apiDef.namespace); |
+ } |
+ if (!isSchemaNodeSupported(eventDef, platform, manifestVersion)) |
+ return; |
+ if (!isSchemaAccessAllowed(eventDef)) { |
+ addUnprivilegedAccessGetter(mod, eventDef.name); |
+ return; |
+ } |
+ |
+ var eventName = apiDef.namespace + "." + eventDef.name; |
+ var options = eventDef.options || {}; |
+ |
+ if (eventDef.filters && eventDef.filters.length > 0) |
+ options.supportsFilters = true; |
+ |
+ if (customEvent) { |
+ mod[eventDef.name] = new customEvent( |
+ eventName, eventDef.parameters, eventDef.extraParameters, |
+ options); |
+ } else if (eventDef.anonymous) { |
+ mod[eventDef.name] = new chrome.Event(); |
+ } else { |
+ mod[eventDef.name] = new chrome.Event( |
+ eventName, eventDef.parameters, options); |
+ } |
+ }); |
+ } |
+ |
+ function addProperties(m, parentDef) { |
+ var properties = parentDef.properties; |
+ if (!properties) |
+ return; |
+ |
+ utils.forEach(properties, function(propertyName, propertyDef) { |
+ if (propertyName in m) |
+ return; // TODO(kalman): be strict like functions/events somehow. |
+ if (!isSchemaNodeSupported(propertyDef, platform, manifestVersion)) |
+ return; |
+ if (!isSchemaAccessAllowed(propertyDef)) { |
+ addUnprivilegedAccessGetter(m, propertyName); |
+ return; |
+ } |
+ |
+ var value = propertyDef.value; |
+ if (value) { |
+ // Values may just have raw types as defined in the JSON, such |
+ // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here. |
+ // TODO(kalman): enforce that things with a "value" property can't |
+ // define their own types. |
+ var type = propertyDef.type || typeof(value); |
+ if (type === 'integer' || type === 'number') { |
+ value = parseInt(value); |
+ } else if (type === 'boolean') { |
+ value = value === "true"; |
+ } else if (propertyDef["$ref"]) { |
+ var refParts = propertyDef["$ref"].split("."); |
+ if (refParts.length > 1) { |
+ var constructor = utils.loadRefDependency( |
+ propertyDef["$ref"])[refParts[refParts.length - 1]]; |
+ } else { |
+ var constructor = customTypes[propertyDef["$ref"]]; |
+ } |
+ if (!constructor) |
+ throw new Error("No custom binding for " + propertyDef["$ref"]); |
+ var args = value; |
+ // For an object propertyDef, |value| is an array of constructor |
+ // arguments, but we want to pass the arguments directly (i.e. |
+ // not as an array), so we have to fake calling |new| on the |
+ // constructor. |
+ value = { __proto__: constructor.prototype }; |
+ constructor.apply(value, args); |
+ // Recursively add properties. |
+ addProperties(value, propertyDef); |
+ } else if (type === 'object') { |
+ // Recursively add properties. |
+ addProperties(value, propertyDef); |
+ } else if (type !== 'string') { |
+ throw new Error("NOT IMPLEMENTED (extension_api.json error): " + |
+ "Cannot parse values for type \"" + type + "\""); |
+ } |
+ m[propertyName] = value; |
+ } |
+ }); |
+ } |
+ |
+ addProperties(mod, apiDef); |
+ // TODO(cduvall): See TODO above about internalAPIs. |
+ if (!apiDef.internal) |
+ return mod; |
+}; |
+ |
+function Bindings(apiName) { |
+ this._schema = schemaRegistry.GetSchema(apiName); |
+ this._customEvent = null; |
+ this._customTypes = {}; |
+ this._customHooks = []; |
+}; |
+ |
+Bindings.prototype = { |
+ // The API through which the ${api_name}_custom_bindings.js files customize |
+ // their API bindings beyond what can be generated. |
+ // |
+ // There are 2 types of customizations available: those which are required in |
+ // order to do the schema generation (registerCustomEvent and |
+ // registerCustomType), and those which can only run after the bindings have |
+ // been generated (registerCustomHook). |
+ // |
+ |
+ // Registers a custom type referenced via "$ref" fields in the API schema |
+ // JSON. |
+ registerCustomType: function(typeName, customTypeFactory) { |
+ var customType = customTypeFactory(); |
+ customType.prototype = new CustomBindingsObject(); |
+ this._customTypes[typeName] = customType; |
+ }, |
+ |
+ // Registers a custom event type for the API identified by |namespace|. |
+ // |event| is the event's constructor. |
+ registerCustomEvent: function(event) { |
+ this._customEvent = event; |
+ }, |
+ |
+ // Registers a function |hook| to run after the schema for all APIs has been |
+ // generated. The hook is passed as its first argument an "API" object to |
+ // interact with, and second the current extension ID. See where |
+ // |customHooks| is used. |
+ registerCustomHook: function(fn) { |
+ this._customHooks.push(fn); |
+ }, |
+ |
+ _runHooks: function(api) { |
+ this._customHooks.forEach(function(hook) { |
+ if (!isSchemaNodeSupported(this._schema, platform, manifestVersion)) |
+ return; |
+ |
+ if (!hook) |
+ return; |
+ |
+ hook({ |
+ apiFunctions: new NamespacedAPIFunctions(this._schema.namespace, |
+ apiFunctions), |
+ schema: this._schema, |
+ compiledApi: api |
+ }, extensionId, contextType); |
+ }, this); |
+ }, |
+ |
+ generate: function() { |
+ var bindings = generate(this._schema, this._customEvent, this._customTypes); |
+ this._runHooks(bindings); |
+ return bindings; |
+ } |
+}; |
+ |
+exports.generate = generate; |
+exports.Bindings = Bindings; |