[self::ANY], 'allow_methods' => [self::ANY], 'allow_headers' => [self::ANY], 'allow_credentials' => false, 'expose_headers' => [], 'max_age' => 0, # 86400 - 1 day 'strict' => false, 'forbidden_message' => '', ]; protected const ANY = '*'; protected const VARY = 'Vary'; /** * @param array $options */ public function __construct(array $options = []) { $this->setOptions( $options ?: (config::get('cors') ?? []) ); } /** * Set cors options * @param array $options */ public function setOptions(array $options) { $this->options = array_merge($this->options, $options); } /** * Handle request * @param RequestInterface $request * @param mixed $next * @return ResponseInterface */ public function __invoke(RequestInterface $request, $next) { return $this->handle($request, $next); } /** * Handle request * @param RequestInterface $request * @param mixed $next * @return ResponseInterface */ public function handle(RequestInterface $request, $next) { # Skip requests without Origin header if (! $request->hasHeader('Origin')) { # Not an access control request return $next($request); } # Preflight Request if ($this->isPreflightRequest($request)) { return $this->setCorsHeaders($request, ResponseFactory::empty(), true); } # Strict request validation if ($this->strict() && ! $this->isAllowedRequest($request)) { return ResponseFactory::createResponse(403, $this->options['forbidden_message'] ?? ''); } return $this->setCorsHeaders($request, $next($request)); } /** * Is preflight request * @param RequestInterface $request * @return bool */ public function isPreflightRequest(RequestInterface $request) { return $request->getMethod() === 'OPTIONS' && $request->hasHeader('Access-Control-Request-Method') && $request->hasHeader('Access-Control-Request-Headers'); } /** * Set CORS response headers * @param RequestInterface $request * @param ResponseInterface $response * @param bool $preflight * @param bool $override * @return ResponseInterface */ public function setCorsHeaders(RequestInterface $request, ResponseInterface $response, bool $preflight = false, bool $override = false) { # Do not override if (! $override && $response->hasHeader('Access-Control-Allow-Origin')) { # Skip if they already exist return $response; } $origin = $this->getAllowOriginHeader($request); if (empty($origin)) { return $response; } if ($preflight) { $headers = [ $origin, $this->getAllowMethodsHeader($request), $this->getAllowHeadersHeader($request), $this->getAllowCredentialsHeader($request), $this->getMaxAgeHeader($request), ]; } else { $headers = [ $origin, $this->getAllowCredentialsHeader($request), $this->getExposeHeadersHeader($request), ]; } foreach ($headers as $header) { foreach ($header as $name => $value) { if (is_array($value) && empty($value)) { continue; } if ($name === self::VARY) { $response = $response->withAddedHeader($name, $value); } else { if ($override || !$response->hasHeader($name)) { $response = $response->withHeader($name, $value); } } } } return $response; } /** * Get Allow-Origin header * @param RequestInterface $request * @return array */ public function getAllowOriginHeader(RequestInterface $request) { $header = 'Access-Control-Allow-Origin'; $allowed = $this->options['allow_origins']; $any = in_array(self::ANY, $allowed); # No origin is allowed if (empty($allowed)) { return []; } # Any origin header is possible only if no credentials allowed if ($any && !$this->allowCredentials()) { return [$header => self::ANY]; } # One origin if (!$any && sizeof($allowed) === 1) { return [$header => reset($allowed)]; } # If any/current origin is allowed - return current one to allow credentials $origin = $request->getHeader('Origin'); $origin = reset($origin); if ($any || in_array($origin, $allowed, true)) { return [ $header => $origin, self::VARY => 'Origin', ]; } return []; } /** * Get Allow-Methods header * @param RequestInterface $request * @return array */ public function getAllowMethodsHeader(RequestInterface $request) { $header = 'Access-Control-Allow-Methods'; $allowed = $this->options['allow_methods']; if (in_array(self::ANY, $allowed)) { return [ $header => $request->getHeader('Access-Control-Request-Method'), self::VARY => $header, ]; } return [ $header => $allowed, ]; } /** * Get Allow-Headers header * @param RequestInterface $request * @return array */ public function getAllowHeadersHeader(RequestInterface $request) { $header = 'Access-Control-Allow-Headers'; $allowed = $this->options['allow_headers']; if (in_array(self::ANY, $allowed)) { return [ $header => $request->getHeader('Access-Control-Request-Headers'), self::VARY => $header, ]; } return [ $header => $allowed, ]; } /** * Get Allow-Credentials header * @param RequestInterface $request * @return array */ public function getAllowCredentialsHeader(RequestInterface $request) { if ($this->allowCredentials()) { return [ 'Access-Control-Allow-Credentials' => 'true', ]; } return []; } /** * Credentials allowed * @return bool */ public function allowCredentials() { return ! empty($this->options['allow_credentials']); } /** * Get Expose-Headers header * @param RequestInterface $request * @return array */ public function getExposeHeadersHeader(RequestInterface $request) { if ($this->options['expose_headers']) { return [ 'Access-Control-Expose-Headers' => $this->options['expose_headers'], ]; } return []; } /** * Get Max-Age header * @param RequestInterface $request * @return array */ public function getMaxAgeHeader(RequestInterface $request) { if ($this->options['max_age']) { return [ 'Access-Control-Max-Age' => $this->options['max_age'], ]; } return []; } /** * Validate CORS'ed request * @param RequestInterface $request * @return bool */ public function isAllowedRequest(RequestInterface $request) { # Skip requests without Origin header if (! $request->hasHeader('Origin')) { return true; } # Method if (! in_array(self::ANY, $this->options['allow_methods'])) { if (! in_array($request->getMethod(), $this->options['allow_methods'], true)) { return false; } } # Origin if ($request->hasHeader('Origin')) { $origin = $request->getHeader('Origin'); $origin = reset($origin); if (!in_array(self::ANY, $this->options['allow_origins'])) { if (! in_array($origin, $this->options['allow_origins'], true)) { return false; } } } return true; } /** * Strict mode enabled * @return bool */ public function strict() { return ! empty($this->options['strict']); } }