diff --git a/lib/classes/component.php b/lib/classes/component.php index 3680ebc1bfe..6fec86efed2 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -547,8 +547,7 @@ class component { // Always keep moodle_exception in place. $keyclasses = [ \core\exception\moodle_exception::class, - \core\output\bootstrap_renderer::class, - \core_cache\cache::class, + \core\router\middleware\api_validation_middleware::class, ]; foreach ($keyclasses as $classname) { if (!array_key_exists($classname, $cache['classmap'])) { diff --git a/lib/classes/router.php b/lib/classes/router.php index f1f3a0e9655..71cbc21b9fc 100644 --- a/lib/classes/router.php +++ b/lib/classes/router.php @@ -16,6 +16,7 @@ namespace core; +use core\router\middleware\api_validation_middleware; use core\router\middleware\cors_middleware; use core\router\middleware\error_handling_middleware; use core\router\middleware\moodle_api_authentication_middleware; @@ -251,7 +252,7 @@ class router { // Add a Middleware to set the CORS headers for all REST Responses. ->add(di::get(cors_middleware::class)) ->add(di::get(moodle_api_authentication_middleware::class)) - ->add(di::get(validation_middleware::class)); + ->add(di::get(api_validation_middleware::class)); } /** diff --git a/lib/classes/router/middleware/api_validation_middleware.php b/lib/classes/router/middleware/api_validation_middleware.php new file mode 100644 index 00000000000..f9582d96d46 --- /dev/null +++ b/lib/classes/router/middleware/api_validation_middleware.php @@ -0,0 +1,49 @@ +. + +namespace core\router\middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * Middleware to handle validation of API requests and responses based on the route data. + * + * @package core + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api_validation_middleware extends validation_middleware { + #[\Override] + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + try { + $request = $this->requestvalidator->validate_request($request); + } catch (\Exception $e) { + return $this->responsehandler->get_response_from_exception($request, $e); + } + + $response = $handler->handle($request); + + try { + $this->responsevalidator->validate_response($request, $response); + } catch (\Exception $e) { + return $this->responsehandler->get_response_from_exception($request, $e); + } + + return $response; + } +} diff --git a/lib/classes/router/middleware/validation_middleware.php b/lib/classes/router/middleware/validation_middleware.php index 05330873a01..4c7034bf9ff 100644 --- a/lib/classes/router/middleware/validation_middleware.php +++ b/lib/classes/router/middleware/validation_middleware.php @@ -53,27 +53,9 @@ class validation_middleware implements MiddlewareInterface { #[\Override] public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - global $USER; - - try { - $request = $this->requestvalidator->validate_request($request); - } catch (\Exception $e) { - $response = $this->responsehandler->get_response_from_exception($request, $e); - // Throw 'page not found' exception for non-admins. - // This hides stacktrace and errorcodes in detailed payload responses. - if (!is_siteadmin($USER->id) && $response->getStatusCode() == 404) { - return \core\router\util::throw_page_not_found($request, $response, $response->getReasonPhrase()); - } - return $response; - } - + $request = $this->requestvalidator->validate_request($request); $response = $handler->handle($request); - - try { - $this->responsevalidator->validate_response($request, $response); - } catch (\Exception $e) { - return $this->responsehandler->get_response_from_exception($request, $e); - } + $this->responsevalidator->validate_response($request, $response); return $response; } diff --git a/lib/classes/router/util.php b/lib/classes/router/util.php index 1c2011feeda..53023119474 100644 --- a/lib/classes/router/util.php +++ b/lib/classes/router/util.php @@ -97,6 +97,37 @@ class util { return self::redirect($response, $url); } + /** + * Throw a specialized HTTP exception based on the response status code. + * + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ResponseInterface $response + */ + public static function throw_specialized_exception( + ServerRequestInterface $request, + ResponseInterface $response, + ?string $message = null, + ?\Throwable $previous = null, + ): void { + $exceptionclass = match ($response->getStatusCode()) { + 400 => \Slim\Exception\HttpBadRequestException::class, + 401 => \Slim\Exception\HttpUnauthorizedException::class, + 403 => \Slim\Exception\HttpForbiddenException::class, + 404 => \Slim\Exception\HttpNotFoundException::class, + 405 => \Slim\Exception\HttpMethodNotAllowedException::class, + 410 => \Slim\Exception\HttpGoneException::class, + 429 => \Slim\Exception\HttpTooManyRequestsException::class, + 500 => \Slim\Exception\HttpInternalServerErrorException::class, + 501 => \Slim\Exception\HttpNotImplementedException::class, + + default => null, + }; + + if ($exceptionclass !== null) { + throw new $exceptionclass($request, $message, $previous); + } + } + /** * Generate a Page Not Found result. * @@ -109,7 +140,7 @@ class util { ServerRequestInterface $request, ResponseInterface $response, ): ResponseInterface { - throw new \Slim\Exception\HttpNotFoundException($request); + self::throw_specialized_exception($request, $response->withStatus(404)); } /** diff --git a/lib/tests/router/middleware/validation_middleware_test.php b/lib/tests/router/middleware/validation_middleware_test.php index 07cdf9ef805..e912a03c073 100644 --- a/lib/tests/router/middleware/validation_middleware_test.php +++ b/lib/tests/router/middleware/validation_middleware_test.php @@ -45,7 +45,7 @@ final class validation_middleware_test extends \advanced_testcase { $requestvalidator->expects($this->once()) ->method('validate_request') ->with($request) - ->willThrowException(new \Exception('Invalid request')); + ->willThrowException(new \Slim\Exception\HttpException($request, 'Invalid request')); // If the request fails validation, it will not be passed to next Middleware. $handler = $this->getMockBuilder(RequestHandlerInterface::class)->getMock(); @@ -60,8 +60,10 @@ final class validation_middleware_test extends \advanced_testcase { // Execute the middleware. $middleware = di::get(validation_middleware::class); - $returns = $middleware->process($request, $handler); - $this->assertInstanceOf(ResponseInterface::class, $returns); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Invalid request'); + $middleware->process($request, $handler); } /** @@ -97,9 +99,10 @@ final class validation_middleware_test extends \advanced_testcase { // Execute the middleware. $middleware = di::get(validation_middleware::class); - $returns = $middleware->process($request, $handler); - $this->assertInstanceOf(ResponseInterface::class, $returns); - $this->assertNotEquals($response, $returns); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Invalid response'); + $middleware->process($request, $handler); } /**