mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-10-31 00:08:07 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			587 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			587 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | |
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /* This file implements a not-quite standard JSON schema validator. It differs
 | |
|  * from the spec in a few ways:
 | |
|  *
 | |
|  *  - the spec doesn't allow custom types to be defined, but this validator
 | |
|  *    defines "URL", "URLorEmpty", "origin" etc.
 | |
|  * - Strings are automatically converted to `URL` objects for the appropriate
 | |
|  *   types.
 | |
|  * - It doesn't support "pattern" when matching strings.
 | |
|  * - The boolean type accepts (and casts) 0 and 1 as valid values.
 | |
|  */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "log", () => {
 | |
|   let { ConsoleAPI } = ChromeUtils.importESModule(
 | |
|     "resource://gre/modules/Console.sys.mjs"
 | |
|   );
 | |
|   return new ConsoleAPI({
 | |
|     prefix: "JsonSchemaValidator",
 | |
|     // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
 | |
|     // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
 | |
|     maxLogLevel: "error",
 | |
|   });
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * To validate a single value, use the static `JsonSchemaValidator.validate`
 | |
|  * method.  If you need to validate multiple values, you instead might want to
 | |
|  * make a JsonSchemaValidator instance with the options you need and then call
 | |
|  * the `validate` instance method.
 | |
|  */
 | |
| export class JsonSchemaValidator {
 | |
|   /**
 | |
|    * Validates a value against a schema.
 | |
|    *
 | |
|    * @param {*} value
 | |
|    *   The value to validate.
 | |
|    * @param {object} schema
 | |
|    *   The schema to validate against.
 | |
|    * @param {boolean} allowArrayNonMatchingItems
 | |
|    *   When true:
 | |
|    *     Invalid items in arrays will be ignored, and they won't be included in
 | |
|    *     result.parsedValue.
 | |
|    *   When false:
 | |
|    *     Invalid items in arrays will cause validation to fail.
 | |
|    * @param {boolean} allowExplicitUndefinedProperties
 | |
|    *   When true:
 | |
|    *     `someProperty: undefined` will be allowed for non-required properties.
 | |
|    *   When false:
 | |
|    *     `someProperty: undefined` will cause validation to fail even for
 | |
|    *     properties that are not required.
 | |
|    * @param {boolean} allowNullAsUndefinedProperties
 | |
|    *   When true:
 | |
|    *     `someProperty: null` will be allowed for non-required properties whose
 | |
|    *     expected types are non-null.
 | |
|    *   When false:
 | |
|    *     `someProperty: null` will cause validation to fail for non-required
 | |
|    *     properties, except for properties whose expected types are null.
 | |
|    * @param {boolean} allowAdditionalProperties
 | |
|    *   When true:
 | |
|    *     Properties that are not defined in the schema will be ignored, and they
 | |
|    *     won't be included in result.parsedValue.
 | |
|    *   When false:
 | |
|    *     Properties that are not defined in the schema will cause validation to
 | |
|    *     fail.
 | |
|    *   Note: Schema objects of type "object" can also contain a boolean property
 | |
|    *     called `additionalProperties` that functions as a local version of this
 | |
|    *     param. When true, extra properties will be allowed in the corresponding
 | |
|    *     input objects regardless of `allowAdditionalProperties`, and as with
 | |
|    *     `allowAdditionalProperties`, extra properties won't be included in
 | |
|    *     `result.parsedValue`. (The inverse is not true: If a schema object
 | |
|    *     defines `additionalProperties: false` but `allowAdditionalProperties`
 | |
|    *     is true, extra properties will be allowed.)
 | |
|    * @return {object}
 | |
|    *   The result of the validation, an object that looks like this:
 | |
|    *
 | |
|    *   {
 | |
|    *     valid,
 | |
|    *     parsedValue,
 | |
|    *     error: {
 | |
|    *       message,
 | |
|    *       rootValue,
 | |
|    *       rootSchema,
 | |
|    *       invalidValue,
 | |
|    *       invalidPropertyNameComponents,
 | |
|    *     }
 | |
|    *   }
 | |
|    *
 | |
|    *   {boolean} valid
 | |
|    *     True if validation is successful, false if not.
 | |
|    *   {*} parsedValue
 | |
|    *     If validation is successful, this is the validated value.  It can
 | |
|    *     differ from the passed-in value in the following ways:
 | |
|    *       * If a type in the schema is "URL" or "URLorEmpty", the passed-in
 | |
|    *         value can use a string instead and it will be converted into a
 | |
|    *         `URL` object in parsedValue.
 | |
|    *       * Some of the `allow*` parameters control the properties that appear.
 | |
|    *         See above.
 | |
|    *   {Error} error
 | |
|    *     If validation fails, `error` will be present.  It contains a number of
 | |
|    *     properties useful for understanding the validation failure.
 | |
|    *   {string} error.message
 | |
|    *     The validation failure message.
 | |
|    *   {*} error.rootValue
 | |
|    *     The passed-in value.
 | |
|    *   {object} error.rootSchema
 | |
|    *     The passed-in schema.
 | |
|    *   {*} invalidValue
 | |
|    *     The value that caused validation to fail.  If the passed-in value is a
 | |
|    *     scalar type, this will be the value itself.  If the value is an object
 | |
|    *     or array, it will be the specific nested value in the object or array
 | |
|    *     that caused validation to fail.
 | |
|    *   {array} invalidPropertyNameComponents
 | |
|    *     If the passed-in value is an object or array, this will contain the
 | |
|    *     names of the object properties or array indexes where invalidValue can
 | |
|    *     be found.  For example, assume the passed-in value is:
 | |
|    *       { foo: { bar: { baz: 123 }}}
 | |
|    *     And assume `baz` should be a string instead of a number.  Then
 | |
|    *     invalidValue will be 123, and invalidPropertyNameComponents will be
 | |
|    *     ["foo", "bar", "baz"], indicating that the erroneous property in the
 | |
|    *     passed-in object is `foo.bar.baz`.
 | |
|    */
 | |
|   static validate(
 | |
|     value,
 | |
|     schema,
 | |
|     {
 | |
|       allowArrayNonMatchingItems = false,
 | |
|       allowExplicitUndefinedProperties = false,
 | |
|       allowNullAsUndefinedProperties = false,
 | |
|       allowAdditionalProperties = false,
 | |
|     } = {}
 | |
|   ) {
 | |
|     let validator = new JsonSchemaValidator({
 | |
|       allowArrayNonMatchingItems,
 | |
|       allowExplicitUndefinedProperties,
 | |
|       allowNullAsUndefinedProperties,
 | |
|       allowAdditionalProperties,
 | |
|     });
 | |
|     return validator.validate(value, schema);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Constructor.
 | |
|    *
 | |
|    * @param {boolean} allowArrayNonMatchingItems
 | |
|    *   See the static `validate` method above.
 | |
|    * @param {boolean} allowExplicitUndefinedProperties
 | |
|    *   See the static `validate` method above.
 | |
|    * @param {boolean} allowNullAsUndefinedProperties
 | |
|    *   See the static `validate` method above.
 | |
|    * @param {boolean} allowAdditionalProperties
 | |
|    *   See the static `validate` method above.
 | |
|    */
 | |
|   constructor({
 | |
|     allowArrayNonMatchingItems = false,
 | |
|     allowExplicitUndefinedProperties = false,
 | |
|     allowNullAsUndefinedProperties = false,
 | |
|     allowAdditionalProperties = false,
 | |
|   } = {}) {
 | |
|     this.allowArrayNonMatchingItems = allowArrayNonMatchingItems;
 | |
|     this.allowExplicitUndefinedProperties = allowExplicitUndefinedProperties;
 | |
|     this.allowNullAsUndefinedProperties = allowNullAsUndefinedProperties;
 | |
|     this.allowAdditionalProperties = allowAdditionalProperties;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Validates a value against a schema.
 | |
|    *
 | |
|    * @param {*} value
 | |
|    *   The value to validate.
 | |
|    * @param {object} schema
 | |
|    *   The schema to validate against.
 | |
|    * @return {object}
 | |
|    *   The result object.  See the static `validate` method above.
 | |
|    */
 | |
|   validate(value, schema) {
 | |
|     return this._validateRecursive(value, schema, [], {
 | |
|       rootValue: value,
 | |
|       rootSchema: schema,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // eslint-disable-next-line complexity
 | |
|   _validateRecursive(param, properties, keyPath, state) {
 | |
|     lazy.log.debug(`checking @${param}@ for type ${properties.type}`);
 | |
| 
 | |
|     if (Array.isArray(properties.type)) {
 | |
|       lazy.log.debug("type is an array");
 | |
|       // For an array of types, the value is valid if it matches any of the
 | |
|       // listed types. To check this, make versions of the object definition
 | |
|       // that include only one type at a time, and check the value against each
 | |
|       // one.
 | |
|       for (const type of properties.type) {
 | |
|         let typeProperties = Object.assign({}, properties, { type });
 | |
|         lazy.log.debug(`checking subtype ${type}`);
 | |
|         let result = this._validateRecursive(
 | |
|           param,
 | |
|           typeProperties,
 | |
|           keyPath,
 | |
|           state
 | |
|         );
 | |
|         if (result.valid) {
 | |
|           return result;
 | |
|         }
 | |
|       }
 | |
|       // None of the types matched
 | |
|       return {
 | |
|         valid: false,
 | |
|         error: new JsonSchemaValidatorError({
 | |
|           message:
 | |
|             `The value '${valueToString(param)}' does not match any type in ` +
 | |
|             valueToString(properties.type),
 | |
|           value: param,
 | |
|           keyPath,
 | |
|           state,
 | |
|         }),
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     switch (properties.type) {
 | |
|       case "boolean":
 | |
|       case "number":
 | |
|       case "integer":
 | |
|       case "string":
 | |
|       case "URL":
 | |
|       case "URLorEmpty":
 | |
|       case "origin":
 | |
|       case "null": {
 | |
|         let result = this._validateSimpleParam(
 | |
|           param,
 | |
|           properties.type,
 | |
|           keyPath,
 | |
|           state
 | |
|         );
 | |
|         if (!result.valid) {
 | |
|           return result;
 | |
|         }
 | |
|         if (properties.enum && typeof result.parsedValue !== "boolean") {
 | |
|           if (!properties.enum.includes(param)) {
 | |
|             return {
 | |
|               valid: false,
 | |
|               error: new JsonSchemaValidatorError({
 | |
|                 message:
 | |
|                   `The value '${valueToString(param)}' is not one of the ` +
 | |
|                   `enumerated values ${valueToString(properties.enum)}`,
 | |
|                 value: param,
 | |
|                 keyPath,
 | |
|                 state,
 | |
|               }),
 | |
|             };
 | |
|           }
 | |
|         }
 | |
|         return result;
 | |
|       }
 | |
| 
 | |
|       case "array":
 | |
|         if (!Array.isArray(param)) {
 | |
|           return {
 | |
|             valid: false,
 | |
|             error: new JsonSchemaValidatorError({
 | |
|               message:
 | |
|                 `The value '${valueToString(param)}' does not match the ` +
 | |
|                 `expected type 'array'`,
 | |
|               value: param,
 | |
|               keyPath,
 | |
|               state,
 | |
|             }),
 | |
|           };
 | |
|         }
 | |
| 
 | |
|         let parsedArray = [];
 | |
|         for (let i = 0; i < param.length; i++) {
 | |
|           let item = param[i];
 | |
|           lazy.log.debug(
 | |
|             `in array, checking @${item}@ for type ${properties.items.type}`
 | |
|           );
 | |
|           let result = this._validateRecursive(
 | |
|             item,
 | |
|             properties.items,
 | |
|             keyPath.concat(i),
 | |
|             state
 | |
|           );
 | |
|           if (!result.valid) {
 | |
|             if (
 | |
|               ("strict" in properties && properties.strict) ||
 | |
|               (!("strict" in properties) && !this.allowArrayNonMatchingItems)
 | |
|             ) {
 | |
|               return result;
 | |
|             }
 | |
|             continue;
 | |
|           }
 | |
| 
 | |
|           parsedArray.push(result.parsedValue);
 | |
|         }
 | |
| 
 | |
|         return { valid: true, parsedValue: parsedArray };
 | |
| 
 | |
|       case "object": {
 | |
|         if (typeof param != "object" || !param) {
 | |
|           return {
 | |
|             valid: false,
 | |
|             error: new JsonSchemaValidatorError({
 | |
|               message:
 | |
|                 `The value '${valueToString(param)}' does not match the ` +
 | |
|                 `expected type 'object'`,
 | |
|               value: param,
 | |
|               keyPath,
 | |
|               state,
 | |
|             }),
 | |
|           };
 | |
|         }
 | |
| 
 | |
|         let parsedObj = {};
 | |
|         let patternProperties = [];
 | |
|         if ("patternProperties" in properties) {
 | |
|           for (let prop of Object.keys(properties.patternProperties || {})) {
 | |
|             let pattern;
 | |
|             try {
 | |
|               pattern = new RegExp(prop);
 | |
|             } catch (e) {
 | |
|               throw new Error(
 | |
|                 `Internal error: Invalid property pattern ${prop}`
 | |
|               );
 | |
|             }
 | |
|             patternProperties.push({
 | |
|               pattern,
 | |
|               schema: properties.patternProperties[prop],
 | |
|             });
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (properties.required) {
 | |
|           for (let required of properties.required) {
 | |
|             if (!(required in param)) {
 | |
|               lazy.log.error(`Object is missing required property ${required}`);
 | |
|               return {
 | |
|                 valid: false,
 | |
|                 error: new JsonSchemaValidatorError({
 | |
|                   message: `Object is missing required property '${required}'`,
 | |
|                   value: param,
 | |
|                   keyPath,
 | |
|                   state,
 | |
|                 }),
 | |
|               };
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         for (let item of Object.keys(param)) {
 | |
|           let schema;
 | |
|           if (
 | |
|             "properties" in properties &&
 | |
|             properties.properties.hasOwnProperty(item)
 | |
|           ) {
 | |
|             schema = properties.properties[item];
 | |
|           } else if (patternProperties.length) {
 | |
|             for (let patternProperty of patternProperties) {
 | |
|               if (patternProperty.pattern.test(item)) {
 | |
|                 schema = patternProperty.schema;
 | |
|                 break;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|           if (!schema) {
 | |
|             let allowAdditionalProperties =
 | |
|               properties.additionalProperties ||
 | |
|               (!properties.strict && this.allowAdditionalProperties);
 | |
|             if (allowAdditionalProperties) {
 | |
|               continue;
 | |
|             }
 | |
|             return {
 | |
|               valid: false,
 | |
|               error: new JsonSchemaValidatorError({
 | |
|                 message: `Object has unexpected property '${item}'`,
 | |
|                 value: param,
 | |
|                 keyPath,
 | |
|                 state,
 | |
|               }),
 | |
|             };
 | |
|           }
 | |
|           let allowExplicitUndefinedProperties =
 | |
|             !properties.strict && this.allowExplicitUndefinedProperties;
 | |
|           let allowNullAsUndefinedProperties =
 | |
|             !properties.strict && this.allowNullAsUndefinedProperties;
 | |
|           let isUndefined =
 | |
|             (!allowExplicitUndefinedProperties && !(item in param)) ||
 | |
|             (allowExplicitUndefinedProperties && param[item] === undefined) ||
 | |
|             (allowNullAsUndefinedProperties && param[item] === null);
 | |
|           if (isUndefined) {
 | |
|             continue;
 | |
|           }
 | |
|           let result = this._validateRecursive(
 | |
|             param[item],
 | |
|             schema,
 | |
|             keyPath.concat(item),
 | |
|             state
 | |
|           );
 | |
|           if (!result.valid) {
 | |
|             return result;
 | |
|           }
 | |
|           parsedObj[item] = result.parsedValue;
 | |
|         }
 | |
|         return { valid: true, parsedValue: parsedObj };
 | |
|       }
 | |
| 
 | |
|       case "JSON":
 | |
|         if (typeof param == "object") {
 | |
|           return { valid: true, parsedValue: param };
 | |
|         }
 | |
|         try {
 | |
|           let json = JSON.parse(param);
 | |
|           if (typeof json != "object") {
 | |
|             return {
 | |
|               valid: false,
 | |
|               error: new JsonSchemaValidatorError({
 | |
|                 message: `JSON was not an object: ${valueToString(param)}`,
 | |
|                 value: param,
 | |
|                 keyPath,
 | |
|                 state,
 | |
|               }),
 | |
|             };
 | |
|           }
 | |
|           return { valid: true, parsedValue: json };
 | |
|         } catch (e) {
 | |
|           lazy.log.error("JSON string couldn't be parsed");
 | |
|           return {
 | |
|             valid: false,
 | |
|             error: new JsonSchemaValidatorError({
 | |
|               message: `JSON string could not be parsed: ${valueToString(
 | |
|                 param
 | |
|               )}`,
 | |
|               value: param,
 | |
|               keyPath,
 | |
|               state,
 | |
|             }),
 | |
|           };
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       valid: false,
 | |
|       error: new JsonSchemaValidatorError({
 | |
|         message: `Invalid schema property type: ${valueToString(
 | |
|           properties.type
 | |
|         )}`,
 | |
|         value: param,
 | |
|         keyPath,
 | |
|         state,
 | |
|       }),
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   _validateSimpleParam(param, type, keyPath, state) {
 | |
|     let valid = false;
 | |
|     let parsedParam = param;
 | |
|     let error = undefined;
 | |
| 
 | |
|     switch (type) {
 | |
|       case "boolean":
 | |
|         if (typeof param == "boolean") {
 | |
|           valid = true;
 | |
|         } else if (typeof param == "number" && (param == 0 || param == 1)) {
 | |
|           valid = true;
 | |
|           parsedParam = !!param;
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case "number":
 | |
|       case "string":
 | |
|         valid = typeof param == type;
 | |
|         break;
 | |
| 
 | |
|       // integer is an alias to "number" that some JSON schema tools use
 | |
|       case "integer":
 | |
|         valid = typeof param == "number";
 | |
|         break;
 | |
| 
 | |
|       case "null":
 | |
|         valid = param === null;
 | |
|         break;
 | |
| 
 | |
|       case "origin":
 | |
|         if (typeof param != "string") {
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           parsedParam = new URL(param);
 | |
| 
 | |
|           if (parsedParam.protocol == "file:") {
 | |
|             // Treat the entire file URL as an origin.
 | |
|             // Note this is stricter than the current Firefox policy,
 | |
|             // but consistent with Chrome.
 | |
|             // See https://bugzilla.mozilla.org/show_bug.cgi?id=803143
 | |
|             valid = true;
 | |
|           } else {
 | |
|             let pathQueryRef = parsedParam.pathname + parsedParam.hash;
 | |
|             // Make sure that "origin" types won't accept full URLs.
 | |
|             if (pathQueryRef != "/" && pathQueryRef != "") {
 | |
|               lazy.log.error(
 | |
|                 `Ignoring parameter "${param}" - origin was expected but received full URL.`
 | |
|               );
 | |
|               valid = false;
 | |
|             } else {
 | |
|               valid = true;
 | |
|             }
 | |
|           }
 | |
|         } catch (ex) {
 | |
|           lazy.log.error(`Ignoring parameter "${param}" - not a valid origin.`);
 | |
|           valid = false;
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case "URL":
 | |
|       case "URLorEmpty":
 | |
|         if (typeof param != "string") {
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         if (type == "URLorEmpty" && param === "") {
 | |
|           valid = true;
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           parsedParam = new URL(param);
 | |
|           valid = true;
 | |
|         } catch (ex) {
 | |
|           if (!param.startsWith("http")) {
 | |
|             lazy.log.error(
 | |
|               `Ignoring parameter "${param}" - scheme (http or https) must be specified.`
 | |
|             );
 | |
|           }
 | |
|           valid = false;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     if (!valid && !error) {
 | |
|       error = new JsonSchemaValidatorError({
 | |
|         message:
 | |
|           `The value '${valueToString(param)}' does not match the expected ` +
 | |
|           `type '${type}'`,
 | |
|         value: param,
 | |
|         keyPath,
 | |
|         state,
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     let result = {
 | |
|       valid,
 | |
|       parsedValue: parsedParam,
 | |
|     };
 | |
|     if (error) {
 | |
|       result.error = error;
 | |
|     }
 | |
|     return result;
 | |
|   }
 | |
| }
 | |
| 
 | |
| class JsonSchemaValidatorError extends Error {
 | |
|   constructor({ message, value, keyPath, state } = {}, ...args) {
 | |
|     if (keyPath.length) {
 | |
|       message +=
 | |
|         ". " +
 | |
|         `The invalid value is property '${keyPath.join(".")}' in ` +
 | |
|         JSON.stringify(state.rootValue);
 | |
|     }
 | |
|     super(message, ...args);
 | |
|     this.name = "JsonSchemaValidatorError";
 | |
|     this.rootValue = state.rootValue;
 | |
|     this.rootSchema = state.rootSchema;
 | |
|     this.invalidPropertyNameComponents = keyPath;
 | |
|     this.invalidValue = value;
 | |
|   }
 | |
| }
 | |
| 
 | |
| function valueToString(value) {
 | |
|   try {
 | |
|     return JSON.stringify(value);
 | |
|   } catch (ex) {}
 | |
|   return String(value);
 | |
| }
 | 
