<commit_before><?php
/**
 * Schema Validator
 *
 * @author      <NAME>
 * @since       1.0
 * @copyright   2017 (c) Iron Bound Designs.
 * @license     GPLv2
 */

namespace IronBound\WP_REST_API\SchemaValidator;

use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\Factory;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\ResourceNotFoundException;
use JsonSchema\Iterator\ObjectIterator;
use JsonSchema\SchemaStorage;
use JsonSchema\Uri\Retrievers\PredefinedArray;
use JsonSchema\Uri\UriRetriever;
use JsonSchema\Validator;

/**
 * Class Middleware
 *
 * @package IronBound\WP_REST_API\SchemaValidator
 */
class Middleware {

	/** @var string */
	private $namespace;

	/** @var array */
	private $strings;

	/** @var int */
	private $check_mode;

	/** @var array[] */
	private $shared_schemas = array();

	/** @var UriRetriever */
	private $uri_retriever;

	/** @var SchemaStorage */
	private $schema_storage;

	/** @var array[] '/wp/v2/posts' => [ 'GET' => 'posts', 'POST' => 'http://...', 'PUT' => 'http://...' ] */
	private $routes_to_schema_urls = array();

	/**
	 * Middleware constructor.
	 *
	 * @param string $namespace
	 * @param array  $strings
	 * @param int    $check_mode Check mode. See Constraint class constants.
	 */
	public function __construct( $namespace, array $strings = array(), $check_mode = 0 ) {
		$this->namespace = trim( $namespace, '/' );
		$this->strings   = wp_parse_args( $strings, array(
			'methodParamDescription' => 'HTTP method to get the schema for. If not provided, will use the base schema.',
			'schemaNotFound'         => 'Schema not found.',
			'expandSchema'           => 'Expand $ref schemas.'
		) );

		if ( $check_mode === 0 ) {
			$check_mode = Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_APPLY_DEFAULTS | Constraint::CHECK_MODE_COERCE_TYPES | Constraint::CHECK_MODE_TYPE_CAST;
		}

		$this->check_mode = $check_mode;
	}

	/**
	 * Initialize the middleware.
	 *
	 * @since 1.0.0
	 */
	public function initialize() {
		add_filter( 'rest_dispatch_request', array( $this, 'validate_and_conform_request' ), 10, 4 );
		add_action( 'rest_api_init', array( $this, 'load_schemas' ), 100 );
		add_filter( 'rest_endpoints', array( $this, 'remove_default_validators_and_set_variable_schemas' ) );
	}

	/**
	 * Deinitialize the middleware and remove filters.
	 *
	 * @since 1.0.0
	 */
	public function deinitialize() {
		remove_filter( 'rest_dispatch_request', array( $this, 'validate_and_conform_request' ), 10 );
		remove_action( 'rest_api_init', array( $this, 'load_schemas' ), 100 );
		remove_filter( 'rest_endpoints', array( $this, 'remove_default_validators_and_set_variable_schemas' ) );
	}

	/**
	 * Add a schema that is not attached to a particular route, but can still be referenced by URL.
	 *
	 * @since 1.0.0
	 *
	 * @param array $schema
	 */
	public function add_shared_schema( array $schema ) {
		$this->shared_schemas[] = $schema;
	}

	/**
	 * After the routes have been registered with the REST server, load all of their schemas into schema storage.
	 *
	 * @since 1.0.0
	 *
	 * @param \WP_REST_Server $server
	 */
	public function load_schemas( \WP_REST_Server $server ) {

		$endpoints      = $this->get_endpoints_for_namespace( $server );
		$schemas        = array();
		$urls_by_method = array();

		foreach ( $endpoints as $route => $handlers ) {

			$options = $server->get_route_options( $route );

			if ( empty( $options['schema'] ) ) {
				if ( empty( $handlers[0]['schema'] ) ) {
					continue;
				}

				$default_schema = call_user_func( $handlers[0]['schema'] );
			} else {
				$default_schema = call_user_func( $options['schema'] );
			}

			if ( empty( $default_schema['title'] ) ) {
				continue;
			}

			$default_title       = $default_schema['title'];
			$default_url         = $this->get_url_for_schema( $default_title );
			$default_schema_json = $this->transform_schema_to_json( $default_schema );

			$urls_by_method[ $route ] = array();

			if ( isset( $handlers['callback'] ) ) {
				$handlers = array( $handlers );
			}

			// Allow for different schemas per HTTP Method.
			foreach ( $handlers as $i => $handler ) {

				foreach ( $handler['methods'] as $method => $_ ) {

					if ( ! isset( $options["schema-{$method}"] ) ) {
						$schemas[ $default_url ]             = $default_schema_json;
						$urls_by_method[ $route ][ $method ] = $default_url;

						continue;
					}

					$method_schema_json = $this->transform_schema_to_json( call_user_func( $options["schema-{$method}"] ) );
					$url                = $this->get_url_for_schema( $default_title, $method );

					$urls_by_method[ $route ][ $method ] = $url;

					$schemas[ $url ] = $method_schema_json;
				}
			}
		}

		foreach ( $this->shared_schemas as $shared_schema ) {
			$schemas[ $this->get_url_for_schema( $shared_schema['title'] ) ] = wp_json_encode( $shared_schema );
		}

		$strategy            = new PredefinedArray( $schemas );
		$this->uri_retriever = new UriRetriever();
		$this->uri_retriever->setUriRetriever( $strategy );

		$this->schema_storage        = new SchemaStorage( $this->uri_retriever );
		$this->routes_to_schema_urls = $urls_by_method;

		$this->register_schema_route();
	}

	/**
	 * Validate a request and conform it to the schema.
	 *
	 * @since 1.0.0
	 *
	 * @param \WP_REST_Response|null|\WP_Error $response
	 * @param \WP_REST_Request                 $request
	 * @param string                           $route
	 * @param array                            $handler
	 *
	 * @return \WP_REST_Response|null|\WP_Error
	 */
	public function validate_and_conform_request( $response, $request, $route, $handler ) {

		if ( $response !== null ) {
			return $response;
		}

		if ( strpos( trim( $route, '/' ), $this->namespace ) !== 0 ) {
			return $response;
		}

		$method = $request->get_method();

		if ( $method === 'PATCH' ) {
			$patch_get_info = $this->get_validate_info_for_method( 'GET', $route, $handler );
			$validated      = $this->validate_and_conform_for_method( $request, $patch_get_info['schema'], 'GET' );

			if ( is_wp_error( $validated ) ) {
				return $validated;
			}
		}

		$info = $this->get_validate_info_for_method( $method, $route, $handler );

		if ( ! $info ) {
			return $response;
		}

		if ( ! empty( $info['described'] ) ) {
			$this->add_described_by( $request, $info['described'] );
		}

		return $this->validate_and_conform_for_method( $request, $info['schema'], $method );
	}

	/**
	 * Get the schema to use for a given method.
	 *
	 * @param string $method
	 * @param string $route
	 * @param array  $handler
	 *
	 * @return array
	 */
	protected function get_validate_info_for_method( $method, $route, $handler ) {
		$map = $this->routes_to_schema_urls;

		if ( $method === 'GET' || $method === 'DELETE' ) {
			$schema_object = json_decode( $this->transform_schema_to_json( array(
				'type'       => 'object',
				'properties' => $handler['args'],
			) ) );
			$described_by  = isset( $map[ $route ], $map[ $route ][ $method ] ) ? $map[ $route ][ $method ] : null;
		} elseif ( isset( $map[ $route ], $map[ $route ][ $method ] ) ) {
			$schema_object = clone $this->schema_storage->getSchema( $map[ $route ][ $method ] );
			$described_by  = $map[ $route ][ $method ];
		} else {
			return array();
		}

		return array(
			'schema'    => $schema_object,
			'described' => $described_by,
		);
	}


	/**
	 * Conform the request or return an error.
	 *
	 * @param \WP_REST_Request $request
	 * @param object           $schema
	 * @param string           $method
	 *
	 * @return null|\WP_Error
	 */
	protected function validate_and_conform_for_method( $request, $schema, $method ) {

		$to_validate = $this->get_params_to_validate( $request, $method );

		if ( ! $to_validate ) {
			return null;
		}

		$validated = $this->validate_params( $to_validate, $schema, $method );

		if ( is_wp_error( $validated ) ) {
			return $validated;
		}

		$this->update_request_params( $request, $validated );

		return null;
	}

	/**
	 * Get the parameters that we should be validating.
	 *
	 * @param \WP_REST_Request $request
	 * @param string           $method
	 *
	 * @return array
	 */
	protected function get_params_to_validate( $request, $method ) {

		$defaults = $request->get_default_params();
		$request->set_default_params( array() );

		if ( $request->get_method() === 'PATCH' && $method === 'PATCH' ) {
			$to_validate = $request->get_json_params() ?: $request->get_body_params();
		} elseif ( $request->get_method() === 'PATCH' && $method === 'GET' ) {
			$to_validate = $request->get_query_params();
		} else {
			$to_validate = $request->get_params();
			
			foreach ( $request->get_url_params() as $param => $value ) {
				unset( $to_validate[ $param ] );
			}
		}

		$request->set_default_params( $defaults );

		return $to_validate;
	}

	/**<commit_msg>Add ability to strictly validate body methods<commit_after><?php
/**
 * Schema Validator
 *
 * @author      Iron Bound Designs
 * @since       1.0
 * @copyright   2017 (c) Iron Bound Designs.
 * @license     GPLv2
 */

namespace IronBound\WP_REST_API\SchemaValidator;

use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\Factory;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\ResourceNotFoundException;
use JsonSchema\Iterator\ObjectIterator;
use JsonSchema\SchemaStorage;
use JsonSchema\Uri\Retrievers\PredefinedArray;
use JsonSchema\Uri\UriRetriever;
use JsonSchema\Validator;

/**
 * Class Middleware
 *
 * @package IronBound\WP_REST_API\SchemaValidator
 */
class Middleware {

	/** @var string */
	private $namespace;

	/** @var array */
	private $strings;

	/** @var int */
	private $check_mode;

	/** @var array */
	private $options;

	/** @var array[] */
	private $shared_schemas = array();

	/** @var UriRetriever */
	private $uri_retriever;

	/** @var SchemaStorage */
	private $schema_storage;

	/** @var array[] '/wp/v2/posts' => [ 'GET' => 'posts', 'POST' => 'http://...', 'PUT' => 'http://...' ] */
	private $routes_to_schema_urls = array();

	/**
	 * Middleware constructor.
	 *
	 * @param string $namespace
	 * @param array  $strings
	 * @param int    $check_mode Check mode. See Constraint class constants.
	 * @param array  $options    Additional options to customize how the middleware behaves.
	 */
	public function __construct( $namespace, array $strings = array(), $check_mode = 0, array $options = [] ) {
		$this->namespace = trim( $namespace, '/' );
		$this->strings   = wp_parse_args( $strings, array(
			'methodParamDescription' => 'HTTP method to get the schema for. If not provided, will use the base schema.',
			'schemaNotFound'         => 'Schema not found.',
			'expandSchema'           => 'Expand $ref schemas.'
		) );

		if ( $check_mode === 0 ) {
			$check_mode = Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_APPLY_DEFAULTS | Constraint::CHECK_MODE_COERCE_TYPES | Constraint::CHECK_MODE_TYPE_CAST;
		}

		$this->check_mode = $check_mode;
		$this->options = $options;
	}

	/**
	 * Initialize the middleware.
	 *
	 * @since 1.0.0
	 */
	public function initialize() {
		add_filter( 'rest_dispatch_request', array( $this, 'validate_and_conform_request' ), 10, 4 );
		add_action( 'rest_api_init', array( $this, 'load_schemas' ), 100 );
		add_filter( 'rest_endpoints', array( $this, 'remove_default_validators_and_set_variable_schemas' ) );
	}

	/**
	 * Deinitialize the middleware and remove filters.
	 *
	 * @since 1.0.0
	 */
	public function deinitialize() {
		remove_filter( 'rest_dispatch_request', array( $this, 'validate_and_conform_request' ), 10 );
		remove_action( 'rest_api_init', array( $this, 'load_schemas' ), 100 );
		remove_filter( 'rest_endpoints', array( $this, 'remove_default_validators_and_set_variable_schemas' ) );
	}

	/**
	 * Add a schema that is not attached to a particular route, but can still be referenced by URL.
	 *
	 * @since 1.0.0
	 *
	 * @param array $schema
	 */
	public function add_shared_schema( array $schema ) {
		$this->shared_schemas[] = $schema;
	}

	/**
	 * After the routes have been registered with the REST server, load all of their schemas into schema storage.
	 *
	 * @since 1.0.0
	 *
	 * @param \WP_REST_Server $server
	 */
	public function load_schemas( \WP_REST_Server $server ) {

		$endpoints      = $this->get_endpoints_for_namespace( $server );
		$schemas        = array();
		$urls_by_method = array();

		foreach ( $endpoints as $route => $handlers ) {

			$options = $server->get_route_options( $route );

			if ( empty( $options['schema'] ) ) {
				if ( empty( $handlers[0]['schema'] ) ) {
					continue;
				}

				$default_schema = call_user_func( $handlers[0]['schema'] );
			} else {
				$default_schema = call_user_func( $options['schema'] );
			}

			if ( empty( $default_schema['title'] ) ) {
				continue;
			}

			$default_title       = $default_schema['title'];
			$default_url         = $this->get_url_for_schema( $default_title );
			$default_schema_json = $this->transform_schema_to_json( $default_schema );

			$urls_by_method[ $route ] = array();

			if ( isset( $handlers['callback'] ) ) {
				$handlers = array( $handlers );
			}

			// Allow for different schemas per HTTP Method.
			foreach ( $handlers as $i => $handler ) {

				foreach ( $handler['methods'] as $method => $_ ) {

					if ( ! isset( $options["schema-{$method}"] ) ) {
						$schemas[ $default_url ]             = $default_schema_json;
						$urls_by_method[ $route ][ $method ] = $default_url;

						continue;
					}

					$method_schema_json = $this->transform_schema_to_json( call_user_func( $options["schema-{$method}"] ) );
					$url                = $this->get_url_for_schema( $default_title, $method );

					$urls_by_method[ $route ][ $method ] = $url;

					$schemas[ $url ] = $method_schema_json;
				}
			}
		}

		foreach ( $this->shared_schemas as $shared_schema ) {
			$schemas[ $this->get_url_for_schema( $shared_schema['title'] ) ] = wp_json_encode( $shared_schema );
		}

		$strategy            = new PredefinedArray( $schemas );
		$this->uri_retriever = new UriRetriever();
		$this->uri_retriever->setUriRetriever( $strategy );

		$this->schema_storage        = new SchemaStorage( $this->uri_retriever );
		$this->routes_to_schema_urls = $urls_by_method;

		$this->register_schema_route();
	}

	/**
	 * Validate a request and conform it to the schema.
	 *
	 * @since 1.0.0
	 *
	 * @param \WP_REST_Response|null|\WP_Error $response
	 * @param \WP_REST_Request                 $request
	 * @param string                           $route
	 * @param array                            $handler
	 *
	 * @return \WP_REST_Response|null|\WP_Error
	 */
	public function validate_and_conform_request( $response, $request, $route, $handler ) {

		if ( $response !== null ) {
			return $response;
		}

		if ( strpos( trim( $route, '/' ), $this->namespace ) !== 0 ) {
			return $response;
		}

		$method = $request->get_method();

		if ( $method === 'PATCH' ) {
			$patch_get_info = $this->get_validate_info_for_method( 'GET', $route, $handler );
			$validated      = $this->validate_and_conform_for_method( $request, $patch_get_info['schema'], 'GET' );

			if ( is_wp_error( $validated ) ) {
				return $validated;
			}
		}

		$info = $this->get_validate_info_for_method( $method, $route, $handler );

		if ( ! $info ) {
			return $response;
		}

		if ( ! empty( $info['described'] ) ) {
			$this->add_described_by( $request, $info['described'] );
		}

		return $this->validate_and_conform_for_method( $request, $info['schema'], $method );
	}

	/**
	 * Get the schema to use for a given method.
	 *
	 * @param string $method
	 * @param string $route
	 * @param array  $handler
	 *
	 * @return array
	 */
	protected function get_validate_info_for_method( $method, $route, $handler ) {
		$map = $this->routes_to_schema_urls;

		if ( $method === 'GET' || $method === 'DELETE' ) {
			$schema_object = json_decode( $this->transform_schema_to_json( array(
				'type'       => 'object',
				'properties' => $handler['args'],
			) ) );
			$described_by  = isset( $map[ $route ], $map[ $route ][ $method ] ) ? $map[ $route ][ $method ] : null;
		} elseif ( isset( $map[ $route ], $map[ $route ][ $method ] ) ) {
			$schema_object = clone $this->schema_storage->getSchema( $map[ $route ][ $method ] );
			$described_by  = $map[ $route ][ $method ];
		} else {
			return array();
		}

		return array(
			'schema'    => $schema_object,
			'described' => $described_by,
		);
	}


	/**
	 * Conform the request or return an error.
	 *
	 * @param \WP_REST_Request $request
	 * @param object           $schema
	 * @param string           $method
	 *
	 * @return null|\WP_Error
	 */
	protected function validate_and_conform_for_method( $request, $schema, $method ) {

		$to_validate = $this->get_params_to_validate( $request, $method );

		/*if ( ! $to_validate ) {
			return null;
		}*/

		$validated = $this->validate_params( $to_validate, $schema, $method );

		if ( is_wp_error( $validated ) ) {
			return $validated;
		}

		$this->update_request_params( $request, $validated );

		return null;
	}

	/**
	 * Get the parameters that we should be validating.
	 *
	 * @param \WP_REST_Request $request
	 * @param string           $method
	 *
	 * @return array
	 */
	protected function get_params_to_validate( $request, $method ) {

		$defaults = $request->get_default_params();
		$request->set_default_params( array() );

		if ( $request->get_method() === 'PATCH' && $method === 'PATCH' ) {
			$to_validate = $request->get_json_params() ?: $request->get_body_params();
		} elseif ( $request->get_method() === 'PATCH' && $method === 'GET' ) {
			$to_validate = $request->get_query_params();
		} elseif ( ! empty( $this->options['strict_body'] ) && ( $request->get_method() === 'POST'  || $request->get_method() === 'PUT' ) ) {
			$to_validate = $request->get_json_params() ?: $request->get_body_params();
		} else {
			$to_validate = $request->get_params();
			
			foreach ( $request->get_url_params() as $param => $value ) {
				unset( $to_validate[ $param ] );
			}
		}

		$request->set_default_params( $defaults );

		return $to_validate;
	}

	/**
{'fraction_non_alphanumeric': 0.1568714341106741, 'fraction_numerical': 0.004320611532709245, 'mean_word_length': 3.3991228070175437, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 126, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7615626', 'n_tokens_mistral': 6739, 'n_tokens_neox': 5964, 'n_words': 1315}