diff options
author | aa@chromium.org <aa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-14 23:40:41 +0000 |
---|---|---|
committer | aa@chromium.org <aa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-14 23:40:41 +0000 |
commit | 4c29efd402105cd29908d21550829bc089a8fe36 (patch) | |
tree | e2e2ac0f5a5d47db3417c1e85205c57ede5e7cb8 /chrome/renderer/resources | |
parent | 5ea8fc38c5cc261077b61469d8ccb158f3e347b8 (diff) | |
download | chromium_src-4c29efd402105cd29908d21550829bc089a8fe36.zip chromium_src-4c29efd402105cd29908d21550829bc089a8fe36.tar.gz chromium_src-4c29efd402105cd29908d21550829bc089a8fe36.tar.bz2 |
Add JsonSchema-based validation for the tab APIs.
Arv: can you take json_schema.js and json_schema_test.js.
Matt: you take the rest.
Review URL: http://codereview.chromium.org/66006
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@13720 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/renderer/resources')
-rw-r--r-- | chrome/renderer/resources/extension_process_bindings.js | 87 | ||||
-rwxr-xr-x | chrome/renderer/resources/json_schema.js | 328 |
2 files changed, 409 insertions, 6 deletions
diff --git a/chrome/renderer/resources/extension_process_bindings.js b/chrome/renderer/resources/extension_process_bindings.js index 3a18c97..ff7ac7e 100644 --- a/chrome/renderer/resources/extension_process_bindings.js +++ b/chrome/renderer/resources/extension_process_bindings.js @@ -1,8 +1,46 @@ var chromium; (function() { + native function GetNextCallbackId(); + native function GetTabsForWindow(); + native function GetTab(); + native function CreateTab(); + native function UpdateTab(); + native function RemoveTab(); + if (!chromium) chromium = {}; + // Validate arguments. + function validate(inst, schemas) { + if (inst.length > schemas.length) + throw new Error("Too many arguments."); + + for (var i = 0; i < schemas.length; i++) { + if (inst[i]) { + var validator = new chromium.JSONSchemaValidator(); + validator.validate(inst[i], schemas[i]); + if (validator.errors.length == 0) + continue; + + var message = "Invalid value for argument " + i + ". "; + 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("Argument " + i + " is required."); + } + } + } + // callback handling var callbacks = []; chromium._dispatchCallback = function(callbackId, str) { @@ -22,7 +60,6 @@ var chromium; var sargs = goog.json.serialize(args); var callbackId = -1; if (callback) { - native function GetNextCallbackId(); callbackId = GetNextCallbackId(); callbacks[callbackId] = callback; } @@ -33,23 +70,61 @@ var chromium; chromium.tabs = {}; // TODO(aa): This should eventually take an optional windowId param. chromium.tabs.getTabsForWindow = function(callback) { - native function GetTabsForWindow(); + validate(arguments, arguments.callee.params); sendRequest(GetTabsForWindow, null, callback); }; + chromium.tabs.getTabsForWindow.params = [ + chromium.types.optFun + ]; + chromium.tabs.getTab = function(tabId, callback) { - native function GetTab(); + validate(arguments, arguments.callee.params); sendRequest(GetTab, tabId, callback); }; + chromium.tabs.getTab.params = [ + chromium.types.pInt, + chromium.types.optFun + ]; + chromium.tabs.createTab = function(tab, callback) { - native function CreateTab(); + validate(arguments, arguments.callee.params); sendRequest(CreateTab, tab, callback); }; + chromium.tabs.createTab.params = [ + { + type: "object", + properties: { + windowId: chromium.types.optPInt, + url: chromium.types.optStr, + selected: chromium.types.optBool + }, + additionalProperties: false + }, + chromium.types.optFun + ]; + chromium.tabs.updateTab = function(tab) { - native function UpdateTab(); + validate(arguments, arguments.callee.params); sendRequest(UpdateTab, tab); }; + chromium.tabs.updateTab.params = [ + { + type: "object", + properties: { + id: chromium.types.pInt, + windowId: chromium.types.optPInt, + url: chromium.types.optStr, + selected: chromium.types.optBool + }, + additionalProperties: false + } + ]; + chromium.tabs.removeTab = function(tabId) { - native function RemoveTab(); + validate(arguments, arguments.callee.params); sendRequest(RemoveTab, tabId); }; + chromium.tabs.removeTab.params = [ + chromium.types.pInt + ]; })(); diff --git a/chrome/renderer/resources/json_schema.js b/chrome/renderer/resources/json_schema.js new file mode 100755 index 0000000..f2d14fb --- /dev/null +++ b/chrome/renderer/resources/json_schema.js @@ -0,0 +1,328 @@ +// Copyright (c) 2009 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 file contains a class that implements a subset of JSON Schema. +// See: http://www.json.com/json-schema-proposal/ for more details. +// +// The following features of JSON Schema are not implemented: +// - requires +// - unique +// - disallow +// - union types +// +// The following properties are not applicable to the interface exposed by +// this class: +// - options +// - readonly +// - title +// - description +// - format +// - default +// - transient +// - hidden +//============================================================================== + +var chromium = chromium || {}; + +/** + * Validates an instance against a schema and accumulates errors. Usage: + * + * var validator = new chromium.JSONSchemaValidator(); + * validator.validate(inst, schema); + * if (validator.errors.length == 0) + * console.log("Valid!"); + * else + * console.log(validator.errors); + * + * The errors property contains a list of objects. Each object has two + * properties: "path" and "message". The "path" property contains the path to + * the key that had the problem, and the "message" property contains a sentence + * describing the error. + */ +chromium.JSONSchemaValidator = function() { + this.errors = []; +}; + +chromium.JSONSchemaValidator.messages = { + invalidEnum: "Value must be one of: [*].", + propertyRequired: "Property is required.", + unexpectedProperty: "Unexpected property.", + arrayMinItems: "Array must have at least * items.", + arrayMaxItems: "Array must not have more than * items.", + itemRequired: "Item is required.", + stringMinLength: "String must be at least * characters long.", + stringMaxLength: "String must not be more than * characters long.", + stringPattern: "String must match the pattern: *.", + numberMinValue: "Value must not be less than *.", + numberMaxValue: "Value must not be greater than *.", + numberMaxDecimal: "Value must not have more than * decimal places.", + invalidType: "Expected '*' but got '*'." +}; + +/** + * Builds an error message. Key is the property in the |errors| object, and + * |opt_replacements| is an array of values to replace "*" characters with. + */ +chromium.JSONSchemaValidator.formatError = function(key, opt_replacements) { + var message = this.messages[key]; + if (opt_replacements) { + for (var i = 0; i < opt_replacements.length; i++) { + message = message.replace("*", opt_replacements[i]); + } + } + return message; +}; + +/** + * Classifies a value as one of the JSON schema primitive types. Note that we + * don't explicitly disallow 'function', because we want to allow functions in + * the input values. + */ +chromium.JSONSchemaValidator.getType = function(value) { + var s = typeof value; + + if (s == "object") { + if (value === null) { + return "null"; + } else if (value instanceof Array || + Object.prototype.toString.call(value) == "[Object Array]") { + return "array"; + } + } else if (s == "number") { + if (value % 1 == 0) { + return "integer"; + } + } + + return s; +}; + +/** + * Validates an instance against a schema. The instance can be any JavaScript + * value and will be validated recursively. When this method returns, the + * |errors| property will contain a list of errors, if any. + */ +chromium.JSONSchemaValidator.prototype.validate = function(instance, schema, + opt_path) { + var path = opt_path || ""; + + // If the schema has an extends property, the instance must validate against + // that schema too. + if (schema.extends) + this.validate(instance, schema.extends, path); + + // If the schema has an enum property, the instance must be one of those + // values. + if (schema.enum) { + if (!this.validateEnum(instance, schema, path)) + return; + } + + if (schema.type && schema.type != "any") { + if (!this.validateType(instance, schema, path)) + return; + + // Type-specific validation. + switch (schema.type) { + case "object": + this.validateObject(instance, schema, path); + break; + case "array": + this.validateArray(instance, schema, path); + break; + case "string": + this.validateString(instance, schema, path); + break; + case "number": + case "integer": + this.validateNumber(instance, schema, path); + break; + } + } +}; + +/** + * Validates an instance against a schema with an enum type. Populates the + * |errors| property, and returns a boolean indicating whether the instance + * validates. + */ +chromium.JSONSchemaValidator.prototype.validateEnum = function(instance, schema, + path) { + for (var i = 0; i < schema.enum.length; i++) { + if (instance === schema.enum[i]) + return true; + } + + this.addError(path, "invalidEnum", [schema.enum.join(", ")]); + return false; +}; + +/** + * Validates an instance against an object schema and populates the errors + * property. + */ +chromium.JSONSchemaValidator.prototype.validateObject = function(instance, + schema, path) { + for (var prop in schema.properties) { + var propPath = path ? path + "." + prop : prop; + if (instance.hasOwnProperty(prop)) { + this.validate(instance[prop], schema.properties[prop], propPath); + } else if (!schema.properties[prop].optional) { + this.addError(propPath, "propertyRequired"); + } + } + + // The additionalProperties property can either be |false| or a schema + // definition. If |false|, additional properties are not allowed. If a schema + // defintion, all additional properties must validate against that schema. + if (typeof schema.additionalProperties != "undefined") { + for (var prop in instance) { + if (instance.hasOwnProperty(prop)) { + var propPath = path ? path + "." + prop : prop; + if (!schema.properties.hasOwnProperty(prop)) { + if (schema.additionalProperties === false) + this.addError(propPath, "unexpectedProperty"); + else + this.validate(instance[prop], schema.additionalProperties, propPath); + } + } + } + } +}; + +/** + * Validates an instance against an array schema and populates the errors + * property. + */ +chromium.JSONSchemaValidator.prototype.validateArray = function(instance, + schema, path) { + var typeOfItems = chromium.JSONSchemaValidator.getType(schema.items); + + if (typeOfItems == 'object') { + if (schema.minItems && instance.length < schema.minItems) { + this.addError(path, "arrayMinItems", [schema.minItems]); + } + + if (typeof schema.maxItems != "undefined" && + instance.length > schema.maxItems) { + this.addError(path, "arrayMaxItems", [schema.maxItems]); + } + + // If the items property is a single schema, each item in the array must + // have that schema. + for (var i = 0; i < instance.length; i++) { + this.validate(instance[i], schema.items, path + "[" + i + "]"); + } + } else if (typeOfItems == 'array') { + // If the items property is an array of schemas, each item in the array must + // validate against the corresponding schema. + for (var i = 0; i < schema.items.length; i++) { + var itemPath = path ? path + "[" + i + "]" : String(i); + if (instance.hasOwnProperty(i)) { + this.validate(instance[i], schema.items[i], itemPath); + } else if (!schema.items[i].optional) { + this.addError(itemPath, "itemRequired"); + } + } + + if (schema.additionalProperties === false) { + if (instance.length > schema.items.length) { + this.addError(path, "arrayMaxItems", [schema.items.length]); + } + } else if (schema.additionalProperties) { + for (var i = schema.items.length; i < instance.length; i++) { + var itemPath = path ? path + "[" + i + "]" : String(i); + this.validate(instance[i], schema.additionalProperties, itemPath); + } + } + } +}; + +/** + * Validates a string and populates the errors property. + */ +chromium.JSONSchemaValidator.prototype.validateString = function(instance, + schema, path) { + if (schema.minLength && instance.length < schema.minLength) + this.addError(path, "stringMinLength", [schema.minLength]); + + if (schema.maxLength && instance.length > schema.maxLength) + this.addError(path, "stringMaxLength", [schema.maxLength]); + + if (schema.pattern && !schema.pattern.test(instance)) + this.addError(path, "stringPattern", [schema.pattern]); +}; + +/** + * Validates a number and populates the errors property. The instance is + * assumed to be a number. + */ +chromium.JSONSchemaValidator.prototype.validateNumber = function(instance, + schema, path) { + if (schema.minimum && instance < schema.minimum) + this.addError(path, "numberMinValue", [schema.minimum]); + + if (schema.maximum && instance > schema.maximum) + this.addError(path, "numberMaxValue", [schema.maximum]); + + if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1) + this.addError(path, "numberMaxDecimal", [schema.maxDecimal]); +}; + +/** + * Validates the primitive type of an instance and populates the errors + * property. Returns true if the instance validates, false otherwise. + */ +chromium.JSONSchemaValidator.prototype.validateType = function(instance, schema, + path) { + var actualType = chromium.JSONSchemaValidator.getType(instance); + if (schema.type != actualType && !(schema.type == "number" && + actualType == "integer")) { + this.addError(path, "invalidType", [schema.type, actualType]); + return false; + } + + return true; +}; + +/** + * Adds an error message. |key| is an index into the |messages| object. + * |replacements| is an array of values to replace '*' characters in the + * message. + */ +chromium.JSONSchemaValidator.prototype.addError = function(path, key, + replacements) { + this.errors.push({ + path: path, + message: chromium.JSONSchemaValidator.formatError(key, replacements) + }); +}; + +// Set up chromium.types with some commonly used types... +(function() { + function extend(base, ext) { + var result = {}; + for (var p in base) + result[p] = base[p]; + for (var p in ext) + result[p] = ext[p]; + return result; + } + + var types = {}; + types.opt = {optional: true}; + types.bool = {type: "boolean"}; + types.int = {type: "integer"}; + types.str = {type: "string"}; + types.fun = {type: "function"}; + types.pInt = extend(types.int, {minimum: 0}); + types.optBool = extend(types.bool, types.opt); + types.optInt = extend(types.int, types.opt); + types.optStr = extend(types.str, types.opt); + types.optFun = extend(types.fun, types.opt); + types.optPInt = extend(types.pInt, types.opt); + + chromium.types = types; +})(); |