blob: 9a8e77a3fcf0df2049da4922989c11aea41f8199 [file] [log] [blame]
// Copyright 2014 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.
var Event = require('event_bindings').Event;
var forEach = require('utils').forEach;
// Note: Beware sneaky getters/setters when using GetAvailbility(). Use safe/raw
// variables as arguments.
var GetAvailability = requireNative('v8_context').GetAvailability;
var exceptionHandler = require('uncaught_exception_handler');
var lastError = require('lastError');
var loadTypeSchema = require('json_schema').loadTypeSchema;
var logActivity = requireNative('activityLogger');
var logging = requireNative('logging');
var process = requireNative('process');
var schemaRegistry = requireNative('schema_registry');
var schemaUtils = require('schemaUtils');
var sendRequestHandler = require('sendRequest');
var contextType = process.GetContextType();
var extensionId = process.GetExtensionId();
var manifestVersion = process.GetManifestVersion();
var platform = process.GetPlatform();
var sendRequest = sendRequestHandler.sendRequest;
// 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(namespace) {
this.apiFunctions_ = { __proto__: null };
this.unavailableApiFunctions_ = { __proto__: null };
this.namespace = namespace;
}
APIFunctions.prototype = {
__proto__: null,
};
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 ($Object.hasOwnProperty(this.unavailableApiFunctions_, apiName))
return;
if (!$Object.hasOwnProperty(this.apiFunctions_, apiName))
throw new Error('Tried to set hook for unknown API "' + apiName + '"');
this.apiFunctions_[apiName][propertyName] = customizedFunction;
};
APIFunctions.prototype.setHandleRequest =
function(apiName, customizedFunction) {
var prefix = this.namespace;
return this.setHook_(apiName, 'handleRequest',
function() {
var ret = $Function.apply(customizedFunction, this, arguments);
// Logs API calls to the Activity Log if it doesn't go through an
// ExtensionFunction.
if (!sendRequestHandler.getCalledSendRequest())
logActivity.LogAPICall(extensionId, prefix + "." + apiName,
$Array.slice(arguments));
return ret;
});
};
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);
};
function isPlatformSupported(schemaNode, platform) {
return !schemaNode.platforms ||
$Array.indexOf(schemaNode.platforms, platform) > -1;
}
function isManifestVersionSupported(schemaNode, manifestVersion) {
return !schemaNode.maximumManifestVersion ||
manifestVersion <= schemaNode.maximumManifestVersion;
}
function isSchemaNodeSupported(schemaNode, platform, manifestVersion) {
return isPlatformSupported(schemaNode, platform) &&
isManifestVersionSupported(schemaNode, manifestVersion);
}
function createCustomType(type) {
var jsModuleName = type.js_module;
logging.CHECK(jsModuleName, 'Custom type ' + type.id +
' has no "js_module" property.');
// This list contains all types that has a js_module property. It is ugly to
// hard-code them here, but the number of APIs that use js_module has not
// changed since the introduction of js_modules in crbug.com/222156.
// This whitelist serves as an extra line of defence to avoid exposing
// arbitrary extension modules when the |type| definition is poisoned.
var whitelistedModules = [
'ChromeSetting',
'ContentSetting',
'EasyUnlockProximityRequired',
'StorageArea',
];
logging.CHECK($Array.indexOf(whitelistedModules, jsModuleName) !== -1,
'Module ' + jsModuleName + ' does not define a custom type.');
var jsModule = require(jsModuleName);
logging.CHECK(jsModule, 'No module ' + jsModuleName + ' found for ' +
type.id + '.');
var customType = jsModule[jsModuleName];
logging.CHECK(customType, jsModuleName + ' must export itself.');
return customType;
}
function Binding(apiName) {
this.apiName_ = apiName;
this.apiFunctions_ = new APIFunctions(apiName);
this.customHooks_ = [];
};
$Object.defineProperty(Binding, 'create', {
__proto__: null,
configurable: false,
enumerable: false,
value: function(apiName) { return new Binding(apiName); },
writable: false,
});
Binding.prototype = {
// Sneaky workaround for Object.prototype getters/setters - our prototype
// isn't Object.prototype. SafeBuiltins (e.g. $Object.hasOwnProperty())
// should still work.
__proto__: null,
// Forward-declare properties.
apiName_: undefined,
apiFunctions_: undefined,
customEvent_: undefined,
customHooks_: undefined,
// 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 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) {
$Array.push(this.customHooks_, fn);
},
// TODO(kalman/cduvall): Refactor this so |runHooks_| is not needed.
runHooks_: function(api, schema) {
$Array.forEach(this.customHooks_, function(hook) {
if (!isSchemaNodeSupported(schema, platform, manifestVersion))
return;
if (!hook)
return;
hook({
__proto__: null,
apiFunctions: this.apiFunctions_,
schema: schema,
compiledApi: api
}, extensionId, contextType);
}, this);
},
// Generates the bindings from the schema for |this.apiName_| and integrates
// any custom bindings that might be present.
generate: function() {
// NB: It's important to load the schema during generation rather than
// setting it beforehand so that we're more confident the schema we're
// loading is real, and not one that was injected by a page intercepting
// Binding.generate.
// Additionally, since the schema is an object returned from a native
// handler, its properties don't have the custom getters/setters that a page
// may have put on Object.prototype, and the object is frozen by v8.
var schema = schemaRegistry.GetSchema(this.apiName_);
function shouldCheckUnprivileged() {
var shouldCheck = 'unprivileged' in schema;
if (shouldCheck)
return shouldCheck;
$Array.forEach(['functions', 'events'], function(type) {
if ($Object.hasOwnProperty(schema, type)) {
$Array.forEach(schema[type], function(node) {
if ('unprivileged' in node)
shouldCheck = true;
});
}
});
if (shouldCheck)
return shouldCheck;
for (var property in schema.properties) {
if ($Object.hasOwnProperty(schema, property) &&
'unprivileged' in schema.properties[property]) {
shouldCheck = true;
break;
}
}
return shouldCheck;
}
var checkUnprivileged = shouldCheckUnprivileged();
// TODO(kalman/cduvall): Make GetAvailability handle this, then delete the
// supporting code.
if (!isSchemaNodeSupported(schema, platform, manifestVersion)) {
console.error('chrome.' + schema.namespace + ' is not supported on ' +
'this platform or manifest version');
return undefined;
}
var mod = {};
var namespaces = $String.split(schema.namespace, '.');
for (var index = 0, name; name = namespaces[index]; index++) {
mod[name] = mod[name] || {};
mod = mod[name];
}
if (schema.types) {
$Array.forEach(schema.types, function(t) {
if (!isSchemaNodeSupported(t, platform, manifestVersion))
return;
// Add types to global schemaValidator; the types we depend on from
// other namespaces will be added as needed.
schemaUtils.schemaValidator.addTypes(t);
// Generate symbols for enums.
var enumValues = t['enum'];
if (enumValues) {
// Type IDs are qualified with the namespace during compilation,
// unfortunately, so remove it here.
logging.DCHECK($String.substr(t.id, 0, schema.namespace.length) ==
schema.namespace);
// Note: + 1 because it ends in a '.', e.g., 'fooApi.Type'.
var id = $String.substr(t.id, schema.namespace.length + 1);
mod[id] = {};
$Array.forEach(enumValues, function(enumValue) {
// Note: enums can be declared either as a list of strings
// ['foo', 'bar'] or as a list of objects
// [{'name': 'foo'}, {'name': 'bar'}].
enumValue = $Object.hasOwnProperty(enumValue, 'name') ?
enumValue.name : enumValue;
if (enumValue) { // Avoid setting any empty enums.
// Make all properties in ALL_CAPS_STYLE.
//
// The built-in versions of $String.replace call other built-ins,
// which may be clobbered. Instead, manually build the property
// name.
//
// If the first character is a digit (we know it must be one of
// a digit, a letter, or an underscore), precede it with an
// underscore.
var propertyName = ($RegExp.exec(/\d/, enumValue[0])) ? '_' : '';
for (var i = 0; i < enumValue.length; ++i) {
var next;
if (i > 0 && $RegExp.exec(/[a-z]/, enumValue[i-1]) &&
$RegExp.exec(/[A-Z]/, enumValue[i])) {
// Replace myEnum-Foo with my_Enum-Foo:
next = '_' + enumValue[i];
} else if ($RegExp.exec(/\W/, enumValue[i])) {
// Replace my_Enum-Foo with my_Enum_Foo:
next = '_';
} else {
next = enumValue[i];
}
propertyName += next;
}
// Uppercase (replace my_Enum_Foo with MY_ENUM_FOO):
propertyName = $String.toUpperCase(propertyName);
mod[id][propertyName] = enumValue;
}
});
}
}, this);
}
// TODO(cduvall): Take out when all APIs have been converted to features.
// 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') ||
schema.unprivileged ||
itemSchema.unprivileged;
};
// Setup Functions.
if (schema.functions) {
$Array.forEach(schema.functions, function(functionDef) {
if (functionDef.name in mod) {
throw new Error('Function ' + functionDef.name +
' already defined in ' + schema.namespace);
}
if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) {
this.apiFunctions_.registerUnavailable(functionDef.name);
return;
}
var apiFunction = { __proto__: null };
apiFunction.definition = functionDef;
apiFunction.name = schema.namespace + '.' + functionDef.name;
if (!GetAvailability(apiFunction.name).is_available ||
(checkUnprivileged && !isSchemaAccessAllowed(functionDef))) {
this.apiFunctions_.registerUnavailable(functionDef.name);
return;
}
// 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 (logging.DCHECK_IS_ON() &&
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.');
}
this.apiFunctions_.register(functionDef.name, apiFunction);
mod[functionDef.name] = $Function.bind(function() {
var args = $Array.slice(arguments);
$Object.setPrototypeOf(args, null);
if (this.updateArgumentsPreValidate)
args = $Function.apply(this.updateArgumentsPreValidate, this, args);
args = schemaUtils.normalizeArgumentsAndValidate(args, this);
if (this.updateArgumentsPostValidate) {
args = $Function.apply(this.updateArgumentsPostValidate,
this,
args);
}
sendRequestHandler.clearCalledSendRequest();
var retval;
if (this.handleRequest) {
retval = $Function.apply(this.handleRequest, this, args);
} else {
var optArgs = {
__proto__: null,
forIOThread: functionDef.forIOThread,
customCallback: this.customCallback
};
retval = sendRequest(this.name, args,
this.definition.parameters,
optArgs);
}
sendRequestHandler.clearCalledSendRequest();
// Validate return value if in sanity check mode.
if (logging.DCHECK_IS_ON() && this.definition.returns)
schemaUtils.validate([retval], [this.definition.returns]);
return retval;
}, apiFunction);
}, this);
}
// Setup Events
if (schema.events) {
$Array.forEach(schema.events, function(eventDef) {
if (eventDef.name in mod) {
throw new Error('Event ' + eventDef.name +
' already defined in ' + schema.namespace);
}
if (!isSchemaNodeSupported(eventDef, platform, manifestVersion))
return;
var eventName = schema.namespace + "." + eventDef.name;
if (!GetAvailability(eventName).is_available ||
(checkUnprivileged && !isSchemaAccessAllowed(eventDef))) {
return;
}
var options = eventDef.options || {};
if (eventDef.filters && eventDef.filters.length > 0)
options.supportsFilters = true;
var parameters = eventDef.parameters;
if (this.customEvent_ && eventDef.extraParameters) {
mod[eventDef.name] = new this.customEvent_(
eventName, parameters, eventDef.extraParameters, options);
} else {
mod[eventDef.name] = new Event(eventName, parameters, options);
}
}, this);
}
function addProperties(m, parentDef) {
var properties = parentDef.properties;
if (!properties)
return;
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 (!GetAvailability(schema.namespace + "." +
propertyName).is_available ||
(checkUnprivileged && !isSchemaAccessAllowed(propertyDef))) {
return;
}
// |value| is eventually added to |m|, the exposed API. Make copies
// of everything from the schema. (The schema is also frozen, so as long
// as we don't make any modifications, shallow copies are fine.)
var value;
if ($Array.isArray(propertyDef.value))
value = $Array.slice(propertyDef.value);
else if (typeof propertyDef.value === 'object')
value = $Object.assign({}, propertyDef.value);
else
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 ref = propertyDef['$ref'];
var type = loadTypeSchema(propertyDef['$ref'], schema);
logging.CHECK(type, 'Schema for $ref type ' + ref + ' not found');
var constructor = createCustomType(type);
var args = value;
logging.DCHECK($Array.isArray(args));
$Array.push(args, type);
// 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 };
$Function.apply(constructor, 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, schema);
// This generate() call is considered successful if any functions,
// properties, or events were created.
var success = ($Object.keys(mod).length > 0);
// Special case: webViewRequest is a vacuous API which just copies its
// implementation from declarativeWebRequest.
//
// TODO(kalman): This would be unnecessary if we did these checks after the
// hooks (i.e. this.runHooks_(mod)). The reason we don't is to be very
// conservative with running any JS which might actually be for an API
// which isn't available, but this is probably overly cautious given the
// C++ is only giving us APIs which are available. FIXME.
if (schema.namespace == 'webViewRequest') {
success = true;
}
// Special case: runtime.lastError is only occasionally set, so
// specifically check its availability.
if (schema.namespace == 'runtime' &&
GetAvailability('runtime.lastError').is_available) {
success = true;
}
if (!success) {
var availability = GetAvailability(schema.namespace);
// If an API was available it should have been successfully generated.
logging.DCHECK(!availability.is_available,
schema.namespace + ' was available but not generated');
console.error('chrome.' + schema.namespace + ' is not available: ' +
availability.message);
return;
}
this.runHooks_(mod, schema);
return mod;
}
};
exports.$set('Binding', Binding);