blob: a935ff9db7e14d2a04668a7c42f63e3f3ae8b218 [file] [log] [blame]
// 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.
// This script contains privileged chrome extension related javascript APIs.
// It is loaded by pages whose URL has the chrome-extension protocol.
var chrome = chrome || {};
(function() {
native function GetChromeHidden();
native function GetExtensionAPIDefinition();
native function GetNextRequestId();
native function StartRequest();
native function SetIconCommon();
var chromeHidden = GetChromeHidden();
if (!chrome)
chrome = {};
function apiExists(path) {
var resolved = chrome;
path.split(".").forEach(function(next) {
if (resolved)
resolved = resolved[next];
});
return !!resolved;
}
function forEach(dict, f) {
for (key in dict) {
if (dict.hasOwnProperty(key))
f(key, dict[key]);
}
}
// Validate arguments.
chromeHidden.validationTypes = [];
chromeHidden.validate = function(args, schemas) {
if (args.length > schemas.length)
throw new Error("Too many arguments.");
for (var i = 0; i < schemas.length; i++) {
if (i in args && args[i] !== null && args[i] !== undefined) {
var validator = new chromeHidden.JSONSchemaValidator();
validator.addTypes(chromeHidden.validationTypes);
validator.validate(args[i], schemas[i]);
if (validator.errors.length == 0)
continue;
var message = "Invalid value for argument " + (i + 1) + ". ";
for (var i = 0, err; err = validator.errors[i]; i++) {
if (err.path) {
message += "Property '" + err.path + "': ";
}
message += err.message;
message = message.substring(0, message.length - 1);
message += ", ";
}
message = message.substring(0, message.length - 2);
message += ".";
throw new Error(message);
} else if (!schemas[i].optional) {
throw new Error("Parameter " + (i + 1) + " is required.");
}
}
};
// Callback handling.
var requests = [];
chromeHidden.handleResponse = function(requestId, name,
success, response, error) {
try {
var request = requests[requestId];
if (success) {
delete chrome.extension.lastError;
} else {
if (!error) {
error = "Unknown error.";
}
console.error("Error during " + name + ": " + error);
chrome.extension.lastError = {
"message": error
};
}
if (request.customCallback) {
request.customCallback(name, request, response);
}
if (request.callback) {
// Callbacks currently only support one callback argument.
var callbackArgs = response ? [chromeHidden.JSON.parse(response)] : [];
// Validate callback in debug only -- and only when the
// caller has provided a callback. Implementations of api
// calls my not return data if they observe the caller
// has not provided a callback.
if (chromeHidden.validateCallbacks && !error) {
try {
if (!request.callbackSchema.parameters) {
throw "No callback schemas defined";
}
if (request.callbackSchema.parameters.length > 1) {
throw "Callbacks may only define one parameter";
}
chromeHidden.validate(callbackArgs,
request.callbackSchema.parameters);
} catch (exception) {
return "Callback validation error during " + name + " -- " +
exception.stack;
}
}
if (response) {
request.callback(callbackArgs[0]);
} else {
request.callback();
}
}
} finally {
delete requests[requestId];
delete chrome.extension.lastError;
}
return undefined;
};
function prepareRequest(args, argSchemas) {
var request = {};
var argCount = args.length;
// Look for callback param.
if (argSchemas.length > 0 &&
args.length == argSchemas.length &&
argSchemas[argSchemas.length - 1].type == "function") {
request.callback = args[argSchemas.length - 1];
request.callbackSchema = argSchemas[argSchemas.length - 1];
--argCount;
}
request.args = [];
for (var k = 0; k < argCount; k++) {
request.args[k] = args[k];
}
return request;
}
// Send an API request and optionally register a callback.
// |opt_args| is an object with optional parameters as follows:
// - noStringify: true if we should not stringify the request arguments.
// - customCallback: a callback that should be called instead of the standard
// callback.
// - nativeFunction: the v8 native function to handle the request, or
// StartRequest if missing.
// - forIOThread: true if this function should be handled on the browser IO
// thread.
function sendRequest(functionName, args, argSchemas, opt_args) {
if (!opt_args)
opt_args = {};
var request = prepareRequest(args, argSchemas);
if (opt_args.customCallback) {
request.customCallback = opt_args.customCallback;
}
// JSON.stringify doesn't support a root object which is undefined.
if (request.args === undefined)
request.args = null;
var sargs = opt_args.noStringify ?
request.args : chromeHidden.JSON.stringify(request.args);
var nativeFunction = opt_args.nativeFunction || StartRequest;
var requestId = GetNextRequestId();
request.id = requestId;
requests[requestId] = request;
var hasCallback =
(request.callback || opt_args.customCallback) ? true : false;
return nativeFunction(functionName, sargs, requestId, hasCallback,
opt_args.forIOThread);
}
// TODO(kalman): It's a shame to need to define this function here, since it's
// only used in 2 APIs (browserAction and pageAction). It would be nice to
// only load this if one of those APIs has been loaded.
// That said, both of those APIs are always injected into pages anyway (see
// chrome/common/extensions/extension_permission_set.cc).
function setIcon(details, name, parameters, actionType) {
var iconSize = 19;
if ("iconIndex" in details) {
sendRequest(name, [details], parameters);
} else if ("imageData" in details) {
// Verify that this at least looks like an ImageData element.
// Unfortunately, we cannot use instanceof because the ImageData
// constructor is not public.
//
// We do this manually instead of using JSONSchema to avoid having these
// properties show up in the doc.
if (!("width" in details.imageData) ||
!("height" in details.imageData) ||
!("data" in details.imageData)) {
throw new Error(
"The imageData property must contain an ImageData object.");
}
if (details.imageData.width > iconSize ||
details.imageData.height > iconSize) {
throw new Error(
"The imageData property must contain an ImageData object that " +
"is no larger than " + iconSize + " pixels square.");
}
sendRequest(name, [details], parameters,
{noStringify: true, nativeFunction: SetIconCommon});
} else if ("path" in details) {
var img = new Image();
img.onerror = function() {
console.error("Could not load " + actionType + " icon '" +
details.path + "'.");
};
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = img.width > iconSize ? iconSize : img.width;
canvas.height = img.height > iconSize ? iconSize : img.height;
var canvas_context = canvas.getContext('2d');
canvas_context.clearRect(0, 0, canvas.width, canvas.height);
canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height);
delete details.path;
details.imageData = canvas_context.getImageData(0, 0, canvas.width,
canvas.height);
sendRequest(name, [details], parameters,
{noStringify: true, nativeFunction: SetIconCommon});
};
img.src = details.path;
} else {
throw new Error(
"Either the path or imageData property must be specified.");
}
}
// 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 = {};
}
APIFunctions.prototype.register = function(apiName, apiFunction) {
this._apiFunctions[apiName] = apiFunction;
};
APIFunctions.prototype._setHook =
function(apiName, propertyName, customizedFunction) {
if (this._apiFunctions.hasOwnProperty(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();
//
// 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.
var customEvents = {};
chromeHidden.registerCustomEvent = function(namespace, event) {
if (typeof(namespace) !== 'string') {
throw new Error("registerCustomEvent requires the namespace of the " +
"API as its first argument");
}
customEvents[namespace] = 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.
var customHooks = {};
chromeHidden.registerCustomHook = function(namespace, fn) {
if (typeof(namespace) !== 'string') {
throw new Error("registerCustomHook requires the namespace of the " +
"API as its first argument");
}
customHooks[namespace] = fn;
};
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.parameters = {};
schema.functions.forEach(function(f) {
self.parameters[f.name] = f.parameters;
});
};
// Registers a custom type referenced via "$ref" fields in the API schema
// JSON.
var customTypes = {};
chromeHidden.registerCustomType = function(typeName, customTypeFactory) {
var customType = customTypeFactory({
sendRequest: sendRequest,
});
customType.prototype = new CustomBindingsObject();
customTypes[typeName] = customType;
};
// 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);
}
chromeHidden.onLoad.addListener(function(extensionId,
isExtensionProcess,
isIncognitoProcess,
manifestVersion) {
var apiDefinitions = GetExtensionAPIDefinition();
// Read api definitions and setup api functions in the chrome namespace.
// TODO(rafaelw): Consider defining a json schema for an api definition
// and validating either here, in a unit_test or both.
// TODO(rafaelw): Handle synchronous functions.
// TODO(rafaelw): Consider providing some convenient override points
// for api functions that wish to insert themselves into the call.
var platform = getPlatform();
apiDefinitions.forEach(function(apiDef) {
if (!isSchemaNodeSupported(apiDef, platform, manifestVersion))
return;
var module = chrome;
var namespaces = apiDef.namespace.split('.');
for (var index = 0, name; name = namespaces[index]; index++) {
module[name] = module[name] || {};
module = module[name];
}
// Add types to global validationTypes
if (apiDef.types) {
apiDef.types.forEach(function(t) {
if (!isSchemaNodeSupported(t, platform, manifestVersion))
return;
chromeHidden.validationTypes.push(t);
if (t.type == 'object' && customTypes[t.id]) {
customTypes[t.id].prototype.setSchema(t);
}
});
}
// Adds a getter that throws an access denied error to object |module|
// for property |name|.
//
// Returns true if the getter was necessary (access is disallowed), or
// false otherwise.
function addUnprivilegedAccessGetter(module, name, allowUnprivileged) {
if (allowUnprivileged || isExtensionProcess)
return false;
module.__defineGetter__(name, function() {
throw new Error(
'"' + name + '" can only be used in extension processes. See ' +
'the content scripts documentation for more details.');
});
return true;
}
// Setup Functions.
if (apiDef.functions) {
apiDef.functions.forEach(function(functionDef) {
if (!isSchemaNodeSupported(functionDef, platform, manifestVersion))
return;
if (functionDef.name in module ||
addUnprivilegedAccessGetter(module, functionDef.name,
functionDef.unprivileged)) {
return;
}
var apiFunction = {};
apiFunction.definition = functionDef;
apiFunction.name = apiDef.namespace + "." + functionDef.name;
apiFunctions.register(apiFunction.name, apiFunction);
module[functionDef.name] = (function() {
var args = arguments;
if (this.updateArgumentsPreValidate)
args = this.updateArgumentsPreValidate.apply(this, args);
chromeHidden.validate(args, this.definition.parameters);
if (this.updateArgumentsPostValidate)
args = this.updateArgumentsPostValidate.apply(this, args);
var retval;
if (this.handleRequest) {
retval = this.handleRequest.apply(this, args);
} else {
retval = sendRequest(this.name, args,
this.definition.parameters,
{customCallback: this.customCallback});
}
// Validate return value if defined - only in debug.
if (chromeHidden.validateCallbacks &&
chromeHidden.validate &&
this.definition.returns) {
chromeHidden.validate([retval], [this.definition.returns]);
}
return retval;
}).bind(apiFunction);
});
}
// Setup Events
if (apiDef.events) {
apiDef.events.forEach(function(eventDef) {
if (!isSchemaNodeSupported(eventDef, platform, manifestVersion))
return;
if (eventDef.name in module ||
addUnprivilegedAccessGetter(module, eventDef.name,
eventDef.unprivileged)) {
return;
}
var eventName = apiDef.namespace + "." + eventDef.name;
var customEvent = customEvents[apiDef.namespace];
if (customEvent) {
module[eventDef.name] = new customEvent(
eventName, eventDef.parameters, eventDef.extraParameters);
} else {
module[eventDef.name] = new chrome.Event(
eventName, eventDef.parameters);
}
});
}
function addProperties(m, def) {
// Parse any values defined for properties.
if (def.properties) {
forEach(def.properties, function(prop, property) {
if (!isSchemaNodeSupported(property, platform, manifestVersion))
return;
if (prop in m ||
addUnprivilegedAccessGetter(m, prop, property.unprivileged)) {
return;
}
var value = property.value;
if (value) {
if (property.type === 'integer') {
value = parseInt(value);
} else if (property.type === 'boolean') {
value = value === "true";
} else if (property["$ref"]) {
var constructor = customTypes[property["$ref"]];
if (!constructor)
throw new Error("No custom binding for " + property["$ref"]);
var args = value;
// For an object property, |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, property);
} else if (property.type === 'object') {
// Recursively add properties.
addProperties(value, property);
} else if (property.type !== 'string') {
throw "NOT IMPLEMENTED (extension_api.json error): Cannot " +
"parse values for type \"" + property.type + "\"";
}
}
if (value) {
m[prop] = value;
}
});
}
}
addProperties(module, apiDef);
});
// TODO(aa): The rest of the crap below this really needs to be factored out
// with a clean API boundary. Right now it is too soupy for me to feel
// comfortable running in content scripts. What if people are just
// overwriting random APIs? That would bypass our content script access
// checks.
if (!isExtensionProcess)
return;
// TODO(kalman/aa): "The rest of this crap..." comment above. Only run the
// custom hooks in extension processes, to maintain current behaviour. We
// should fix this this with a smaller hammer.
apiDefinitions.forEach(function(apiDef) {
if (!isSchemaNodeSupported(apiDef, platform, manifestVersion))
return;
var hook = customHooks[apiDef.namespace];
if (!hook)
return;
// Pass through the public API of schema_generated_bindings, to be used
// by custom bindings JS files. Create a new one so that bindings can't
// interfere with each other.
hook({
apiFunctions: apiFunctions,
sendRequest: sendRequest,
setIcon: setIcon,
}, extensionId);
});
// TOOD(mihaip): remove this alias once the webstore stops calling
// beginInstallWithManifest2.
// See http://crbug.com/100242
if (apiExists("webstorePrivate")) {
chrome.webstorePrivate.beginInstallWithManifest2 =
chrome.webstorePrivate.beginInstallWithManifest3;
}
if (apiExists("test"))
chrome.test.getApiDefinitions = GetExtensionAPIDefinition;
});
})();