Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  JsonSchemaValidator.sys.mjs   Sprache: unbekannt

 
/* 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);
}

[ Dauer der Verarbeitung: 0.3 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge