<commit_before><?php

namespace RREST;

use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use JsonSchema\Validator;
use RREST\APISpec\APISpecInterface;
use RREST\Provider\ProviderInterface;
use RREST\Exception\InvalidParameterException;
use RREST\Exception\InvalidBodyException;

/**
 * ApiSpec + Provider = RREST.
 */
class RREST
{
    /**
     * @var APISpecInterface
     */
    protected $apiSpec;

    /**
     * @var ProviderInterface
     */
    protected $provider;

    /**
     * @var string
     */
    protected $controllerNamespace;

    /**
     * @var array
     */
    protected $hintedHTTPParameters;

    /**
     * @var array|stdClass
     */
    protected $hintedPayloadBody;

    /**
     * @param APISpecInterface  $apiSpec
     * @param ProviderInterface $provider
     * @param string            $controllerNamespace
     */
    public function __construct(APISpecInterface $apiSpec, ProviderInterface $provider, $controllerNamespace = 'Controllers')
    {
        $this->apiSpec = $apiSpec;
        $this->provider = $provider;
        $this->controllerNamespace = $controllerNamespace;
        $this->hintedHTTPParameters = [];
    }

    public function addRoute()
    {
        $routePath = $this->apiSpec->getRoutePath();
        $method = $this->apiSpec->getRouteMethod();
        $controllerClassName = $this->getRouteControllerClassName(
            $this->apiSpec->getRessourcePath()
        );

        $this->assertControllerClassName($controllerClassName);
        $this->assertActionMethodName($controllerClassName, $method);

        $this->provider->addRoute(
            $routePath,
            $method,
            $this->getControllerNamespaceClass($controllerClassName),
            $this->getActionMethodName($method),
            function () {
                $this->assertHTTPProtocol();
                $this->assertHTTPContentType();
                $this->assertHTTPParameters();
                $this->assertHTTPPayloadBody();
                $this->hintHTTPParameterValue();
                $this->hintHTTPPayloadBody();
            }
        );
    }

    /**
     * @param string $origin
     * @param string $methods
     * @param string $headers
     *
     * @return bool
     */
    public function applyCORS($origin = '*', $methods = 'GET,POST,PUT,DELETE,OPTIONS', $headers = '')
    {
        return $this->provider->applyCORS($origin, $methods, $headers);
    }

    /**
     * @throw AccessDeniedHttpException
     */
    protected function assertHTTPProtocol()
    {
        $supportedHTTPProtocols = array_map('strtoupper', $this->apiSpec->getSupportedHTTPProtocols());
        $httpProtocol = strtoupper($this->provider->getHTTPProtocol());
        if(in_array($httpProtocol, $supportedHTTPProtocols) === false) {
            throw new AccessDeniedHttpException();
        }
    }

    /**
     * @throw UnsupportedMediaTypeHttpException
     */
    protected function assertHTTPContentType()
    {
        if( in_array($this->provider->getContentType(), $this->apiSpec->getContentTypes()) === false ) {
            throw new UnsupportedMediaTypeHttpException();
        }
    }

    /**
     * @throw InvalidParameterException
     */
    protected function assertHTTPParameters()
    {
        $invalidParametersError = [];
        $parameters = $this->apiSpec->getParameters();
        foreach ($parameters as $parameter) {
            $value = $this->provider->getHTTPParameterValue(
                $parameter->getName(),
                $parameter->getType()
            );
            try {
                $castValue = $this->cast($value, $parameter->getType());
            } catch (\Exception $e) {
                throw new InvalidParameterException([
                    new Error(
                        $e->getMessage(),
                        $e->getCode()
                    ),
                ]);
            }
            try {
                $parameter->assertValue($castValue, $value);
                $this->hintedHTTPParameters[$parameter->getName()] = $castValue;
            } catch (InvalidParameterException $e) {
                $invalidParametersError = array_merge(
                    $e->getErrors(),
                    $invalidParametersError
                );
            }
        }

        if (empty($invalidParametersError) == false) {
            throw new InvalidParameterException($invalidParametersError);
        }
    }

    protected function hintHTTPParameterValue()
    {
        foreach ($this->hintedHTTPParameters as $key => $value) {
            $this->provider->setHTTPParameterValue($key, $value);
        }
    }

    /**
     * @throw RREST\Exception\InvalidBodyException
     * @throw RREST\Exception\InvalidParameterException
     */
    protected function assertHTTPPayloadBody()
    {
        //FIXME: split/refactor the code
        //FIXME: handle XML
        $payloadBodySchema = $this->apiSpec->getPayloadBodySchema(
            $this->provider->getContentType()
        );

        //No payload body here, on need to assert
        if($payloadBodySchema === false) {
            return;
        }

        //validate json
        $payloadBodyValueJSON = json_decode($this->provider->getHTTPPayloadBodyValue());
        if (json_last_error() !== JSON_ERROR_NONE) {
            $error = new Error();
            $error->message = ucfirst(json_last_error_msg());
            $error->code = 50;
            throw new InvalidBodyException([$error]);
        }

        //validate JsonSchema
        $invalidBodyError = [];
        $jsonValidator = new Validator();
        $jsonValidator->check($payloadBodyValueJSON, json_decode($payloadBodySchema));
        if ($jsonValidator->isValid() === false) {
            foreach ($jsonValidator->getErrors() as $jsonError) {
                $error = new Error();
                $error->message = ucfirst(
                    trim(
                        strtolower(
                            $jsonError['property'].' property: '.$jsonError['message']
                        )
                    )
                );
                $error->code = 52;
                $invalidBodyError[] = $error;
            }

            if (empty($invalidBodyError) == false) {
                throw new InvalidParameterException($invalidBodyError);
            }
        }

        $this->hintedPayloadBody= $payloadBodyValueJSON;
    }

    protected function hintHTTPPayloadBody()
    {
        $this->provider->setHTTPPayloadBodyValue( $this->hintedPayloadBody );
    }

    /**
     * @param string $controllerClassName
     * @throw RuntimeException
     *
     * @return string
     */
    protected function assertControllerClassName($controllerClassName)
    {
        $controllerNamespaceClass = $this->getControllerNamespaceClass($controllerClassName);<commit_msg>refactor: split assertHTTPPayloadBody into more methods & prepare for XML support<commit_after><?php

namespace RREST;

use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use JsonSchema\Validator;
use RREST\APISpec\APISpecInterface;
use RREST\Provider\ProviderInterface;
use RREST\Exception\InvalidParameterException;
use RREST\Exception\InvalidBodyException;

/**
 * ApiSpec + Provider = RREST.
 */
class RREST
{
    /**
     * @var APISpecInterface
     */
    protected $apiSpec;

    /**
     * @var ProviderInterface
     */
    protected $provider;

    /**
     * @var string
     */
    protected $controllerNamespace;

    /**
     * @var array
     */
    protected $hintedHTTPParameters;

    /**
     * @var array|stdClass
     */
    protected $hintedPayloadBody;

    /**
     * @param APISpecInterface  $apiSpec
     * @param ProviderInterface $provider
     * @param string            $controllerNamespace
     */
    public function __construct(APISpecInterface $apiSpec, ProviderInterface $provider, $controllerNamespace = 'Controllers')
    {
        $this->apiSpec = $apiSpec;
        $this->provider = $provider;
        $this->controllerNamespace = $controllerNamespace;
        $this->hintedHTTPParameters = [];
    }

    public function addRoute()
    {
        $routePath = $this->apiSpec->getRoutePath();
        $method = $this->apiSpec->getRouteMethod();
        $controllerClassName = $this->getRouteControllerClassName(
            $this->apiSpec->getRessourcePath()
        );

        $this->assertControllerClassName($controllerClassName);
        $this->assertActionMethodName($controllerClassName, $method);

        $this->provider->addRoute(
            $routePath,
            $method,
            $this->getControllerNamespaceClass($controllerClassName),
            $this->getActionMethodName($method),
            function () {
                $this->assertHTTPProtocol();
                $this->assertHTTPContentType();
                $this->assertHTTPParameters();
                $this->assertHTTPPayloadBody();
                $this->hintHTTPParameterValue($this->hintedHTTPParameters);
                $this->hintHTTPPayloadBody($this->hintedPayloadBody);
            }
        );
    }

    /**
     * @param string $origin
     * @param string $methods
     * @param string $headers
     *
     * @return bool
     */
    public function applyCORS($origin = '*', $methods = 'GET,POST,PUT,DELETE,OPTIONS', $headers = '')
    {
        return $this->provider->applyCORS($origin, $methods, $headers);
    }

    /**
     * @throw AccessDeniedHttpException
     */
    protected function assertHTTPProtocol()
    {
        $supportedHTTPProtocols = array_map('strtoupper', $this->apiSpec->getSupportedHTTPProtocols());
        $httpProtocol = strtoupper($this->provider->getHTTPProtocol());
        if(in_array($httpProtocol, $supportedHTTPProtocols) === false) {
            throw new AccessDeniedHttpException();
        }
    }

    /**
     * @throw UnsupportedMediaTypeHttpException
     */
    protected function assertHTTPContentType()
    {
        if( in_array($this->provider->getContentType(), $this->apiSpec->getContentTypes()) === false ) {
            throw new UnsupportedMediaTypeHttpException();
        }
    }

    /**
     * @throw InvalidParameterException
     */
    protected function assertHTTPParameters()
    {
        $invalidParametersError = [];
        $parameters = $this->apiSpec->getParameters();
        foreach ($parameters as $parameter) {
            $value = $this->provider->getHTTPParameterValue(
                $parameter->getName(),
                $parameter->getType()
            );
            try {
                $castValue = $this->cast($value, $parameter->getType());
            } catch (\Exception $e) {
                throw new InvalidParameterException([
                    new Error(
                        $e->getMessage(),
                        $e->getCode()
                    ),
                ]);
            }
            try {
                $parameter->assertValue($castValue, $value);
                $this->hintedHTTPParameters[$parameter->getName()] = $castValue;
            } catch (InvalidParameterException $e) {
                $invalidParametersError = array_merge(
                    $e->getErrors(),
                    $invalidParametersError
                );
            }
        }

        if (empty($invalidParametersError) == false) {
            throw new InvalidParameterException($invalidParametersError);
        }
    }

    protected function hintHTTPParameterValue($hintedHTTPParameters)
    {
        foreach ($hintedHTTPParameters as $key => $value) {
            $this->provider->setHTTPParameterValue($key, $value);
        }
    }

    /**
     * @throw RREST\Exception\InvalidBodyException
     */
    protected function assertHTTPPayloadBody()
    {
        $httpContentType = $this->provider->getContentType();
        $payloadBodySchema = $this->apiSpec->getPayloadBodySchema($httpContentType);

        //No payload body here, no need to assert
        if($payloadBodySchema === false) {
            return;
        }

        $payloadBodyValue = $this->provider->getHTTPPayloadBodyValue();

        switch (true) {
            case strpos($httpContentType, 'json') !== false:
                $this->assertHTTPPayloadBodyJSON($payloadBodyValue, $payloadBodySchema);
                break;
            case strpos($httpContentType, 'xml') !== false:
                $this->assertHTTPPayloadBodyXML($payloadBodyValue, $payloadBodySchema);
                break;
            default:
                throw new UnsupportedMediaTypeHttpException();
                break;
        }
    }

    /**
     * @param  string $payloadBodyValue
     * @param  string $payloadBodySchema
     *
     * @throws RREST\Exception\InvalidBodyException
     *
     */
    protected function assertHTTPPayloadBodyXML($payloadBodyValue, $payloadBodySchema)
    {
        $thowInvalidBodyException = function() {
            $invalidBodyError = [];
            $libXMLErrors = libxml_get_errors();
            libxml_clear_errors();
            if (empty($libXMLErrors) === false) {
                foreach ($libXMLErrors as $libXMLError) {
                    $message = $libXMLError->message.' (line: '.$libXMLError->line.')';
                    $invalidBodyError[] = new Error(
                        $message,
                        'invalid-payloadbody-xml'
                    );
                }
                if (empty($invalidBodyError) == false) {
                    throw new InvalidBodyException($invalidBodyError);
                }
            }
        };

        //validate XML
        $originalErrorLevel = libxml_use_internal_errors(true);
        $dom = new \DOMDocument;
        $dom->loadXML($payloadBodyValue);
        $thowInvalidBodyException();

        //validate XMLSchema
        $invalidBodyError = [];
        $dom->schemaValidateSource($payloadBodySchema);
        $thowInvalidBodyException();

        libxml_use_internal_errors($originalErrorLevel);
        $this->hintedPayloadBody= $payloadBodyValueXML;
    }

    /**
     * @param  string $payloadBodyValue
     * @param  string $payloadBodySchema
     *
     * @throws RREST\Exception\InvalidBodyException
     *
     */
    protected function assertHTTPPayloadBodyJSON($payloadBodyValue, $payloadBodySchema)
    {
        //validate JSON
        $payloadBodyValueJSON = json_decode($payloadBodyValue);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new InvalidBodyException([new Error(
                ucfirst(json_last_error_msg()),
                'invalid-payloadbody-json'
            )]);
        }

        //validate JsonSchema
        $jsonValidator = new Validator();
        $jsonValidator->check($payloadBodyValueJSON, json_decode($payloadBodySchema));
        if ($jsonValidator->isValid() === false) {
            $invalidBodyError = [];
            foreach ($jsonValidator->getErrors() as $jsonError) {
                $invalidBodyError[] = new Error(
                    ucfirst(trim(strtolower(
                        $jsonError['property'].' property: '.$jsonError['message']
                    ))),
                    'invalid-payloadbody-json'
                );
            }
            if (empty($invalidBodyError) == false) {
                throw new InvalidBodyException($invalidBodyError);
            }
        }

        $this->hintedPayloadBody= $payloadBodyValueJSON;
    }

    protected function hintHTTPPayloadBody($hintedPayloadBody)
    {
        $this->provider->setHTTPPayloadBodyValue( $hintedPayloadBody );
    }

    /**
     * @param string $controllerClassName
     * @throw RuntimeException
     *
     * @return string
     */
    protected function assertControllerClassName($controllerClassName)
    {
        $controllerNamespaceClass = $this->getControllerNamespaceClass($controllerClassName);
{'fraction_non_alphanumeric': 0.11619784195097611, 'fraction_numerical': 0.0002494854362876567, 'mean_word_length': 2.07518220176448, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 144, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, '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': '565408', 'n_tokens_mistral': 4104, 'n_tokens_neox': 4009, 'n_words': 676}