Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
CorsMiddleware
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
3 / 3
4
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 attachCorsHeaders
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Api\Middleware;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use Psr\Http\Message\ResponseFactoryInterface;
12use Psr\Http\Message\ResponseInterface;
13use Psr\Http\Message\ServerRequestInterface;
14use Psr\Http\Server\MiddlewareInterface;
15use Psr\Http\Server\RequestHandlerInterface;
16
17/**
18 * Cross-Origin Resource Sharing (CORS) Middleware (OWASP ASVS 13.2.1 / OWASP API8:2023).
19 *
20 * Handles pre-flight OPTIONS requests and applies standard CORS headers to API responses.
21 *
22 * @package App\Core\Api\Middleware
23 */
24final readonly class CorsMiddleware implements MiddlewareInterface
25{
26    /** @var string Default allowed methods list. */
27    private const string DEFAULT_METHODS = 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
28
29    /** @var string Default allowed headers list. */
30    private const string DEFAULT_HEADERS = 'Authorization, Content-Type, X-Request-ID, '
31        . 'Idempotency-Key, If-None-Match, X-CSRF-Token';
32
33    /**
34     * CorsMiddleware constructor.
35     *
36     * @param ResponseFactoryInterface $responseFactory PSR-17 response factory.
37     * @param string                   $allowedOrigins  Allowed origins (default '*').
38     * @param string                   $allowedMethods  Allowed HTTP methods.
39     * @param string                   $allowedHeaders  Allowed headers.
40     * @param int                      $maxAge          Preflight cache max age in seconds.
41     */
42    public function __construct(
43        private ResponseFactoryInterface $responseFactory,
44        private string $allowedOrigins = '*',
45        private string $allowedMethods = self::DEFAULT_METHODS,
46        private string $allowedHeaders = self::DEFAULT_HEADERS,
47        private int $maxAge = 86400
48    ) {
49    }
50
51    /**
52     * Processes incoming request and applies CORS policy.
53     *
54     * @param ServerRequestInterface  $request Server request.
55     * @param RequestHandlerInterface $handler Request handler.
56     * @return ResponseInterface Response with CORS headers.
57     */
58    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
59    {
60        if ($request->getMethod() === 'OPTIONS') {
61            $response = $this->responseFactory->createResponse(204);
62
63            return $this->attachCorsHeaders($response);
64        }
65
66        $response = $handler->handle($request);
67
68        return $this->attachCorsHeaders($response);
69    }
70
71    /**
72     * Attaches standard CORS headers to response.
73     *
74     * @param ResponseInterface $response Target response.
75     * @return ResponseInterface Augmented response.
76     */
77    private function attachCorsHeaders(ResponseInterface $response): ResponseInterface
78    {
79        return $response
80            ->withHeader('Access-Control-Allow-Origin', $this->allowedOrigins)
81            ->withHeader('Access-Control-Allow-Methods', $this->allowedMethods)
82            ->withHeader('Access-Control-Allow-Headers', $this->allowedHeaders)
83            ->withHeader('Access-Control-Max-Age', (string) $this->maxAge);
84    }
85}