From 397cf6795ea0ae27977a4df5d6e5af7c14f437bd Mon Sep 17 00:00:00 2001 From: Philippe Gaultier Date: Thu, 3 Jul 2014 17:39:09 +0200 Subject: [PATCH] Adding CORS filter to allow Cross origin resource Sharing --- framework/filters/Cors.php | 242 +++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 framework/filters/Cors.php diff --git a/framework/filters/Cors.php b/framework/filters/Cors.php new file mode 100644 index 0000000000..52b8057cec --- /dev/null +++ b/framework/filters/Cors.php @@ -0,0 +1,242 @@ + [ + * 'class' => \yii\filters\Cors::className(), + * ], + * ]; + * } + * ``` + * + * The CORS filter can be specialized to restrict parameters, like this, + * [MDN CORS Information](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) + * + * ```php + * public function behaviors() + * { + * return [ + * 'corsFilter' => [ + * 'class' => \yii\filters\Cors::className(), + * // restrict access to + * 'Origin' => ['http://www.myserver.com', 'https://www.myserver.com'], + * 'Access-Control-Request-Method' => ['POST', 'PUT'], + * // Allow only POST and PUT methods + * 'Access-Control-Request-Headers' => ['X-Wsse'], + * // Allow only headers 'X-Wsse' + * 'Access-Control-Allow-Credentials' => true, + * // Allow OPTIONS caching + * 'Access-Control-Max-Age' => 3600, + * + * ], + * ]; + * } + * ``` + * + * + * @author Philippe Gaultier + * @since 2.0 + */ +class Cors extends ActionFilter +{ + /** + * @var Request the current request. If not set, the `request` application component will be used. + */ + public $request; + /** + * @var Response the response to be sent. If not set, the `response` application component will be used. + */ + public $response; + /** + * @var array define specific CORS rules for specific actions + */ + public $actions = []; + /** + * @var array Basic headers handled for the CORS requests. + */ + public $cors = [ + 'Origin' => ['*'], + 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], + 'Access-Control-Request-Headers' => ['*'], + 'Access-Control-Allow-Credentials' => true, + 'Access-Control-Max-Age' => 86400, + ]; + + /** + * @inheritdoc + */ + public function beforeAction($action) + { + $this->request = Yii::$app->getRequest(); + $this->response = Yii::$app->getResponse(); + $this->overrideSettings($action); + + $requestCorsHeaders = $this->extractHeaders($this->request); + $responseCorsHeaders = $this->prepareHeaders($requestCorsHeaders); + $this->addCorsHeaders($this->response, $responseCorsHeaders); + + return true; + } + + /** + * Override settings for current action + * @param \yii\base\Action $action the action settings to override + */ + public function overrideSettings($action) + { + if (isset($this->actions[$action->id])) { + $actionParams = $this->actions[$action->id]; + foreach ($this->cors as $headerField => $headerValue) { + if (isset($actionParams[$headerField])) { + $this->cors[$headerField] = $actionParams[$headerField]; + } + } + } + } + + /** + * Extract CORS headers fron the request + * @param Request $request + * @return array CORS headers to handle + */ + public function extractHeaders($request) + { + $headers = []; + $requestHeaders = array_keys($this->cors); + foreach ($requestHeaders as $headerField) { + $serverField = static::headerizeToPhp($headerField); + $headerData = isset($_SERVER[$serverField])?$_SERVER[$serverField]:null; + if ($headerData !== null) { + $headers[$headerField] = $headerData; + } + } + return $headers; + } + + /** + * Handle classic CORS request to avoid duplicate code + * @param string $type the kind of headers we would handle + * @param array $requestHeaders CORS headers request by client + * @param array $responseHeaders CORS response headers sent to the clinet + */ + protected function prepareAllowHeaders($type, $requestHeaders, &$responseHeaders) + { + $requestHeaderField = 'Access-Control-Request-'.$type; + $responseHeaderField = 'Access-Control-Allow-'.$type; + if (isset($requestHeaders[$requestHeaderField])) { + if (in_array('*', $this->cors[$requestHeaderField])) { + if ($type === 'Method') { + $responseHeaders[$responseHeaderField] = strtoupper($responseHeaders[$responseHeaderField]); + } elseif ($type === 'Headers') { + $responseHeaders[$responseHeaderField] = static::headerize($responseHeaders[$responseHeaderField]); + } + } else { + $requestedData = preg_split("/[\s,]+/", $requestHeaders[$requestHeaderField], -1, PREG_SPLIT_NO_EMPTY); + $acceptedData = []; + foreach ($requestedData as $req) { + if ($type === 'Method') { + $req = strtoupper($req); + } elseif ($type === 'Headers') { + // ucwords + $req = static::headerize($req); + } + if (in_array($req, $this->cors[$requestHeaderField])) { + $acceptedData[] = $req; + } + } + if (empty($acceptedData) === false) { + $responseHeaders[$responseHeaderField] = implode(', ', $acceptedData); + } + } + } + } + + /** + * Convert any string (including php headers with HTTP prefix) to header format like : + * * X-PINGOTHER -> X-Pingother + * * HTTP_X_PINGOTHER -> X-Pingother + * @param string $string string to convert + * @return string the result in "header" format + */ + protected static function headerize($string) + { + return str_replace(' ', '-', ucwords(strtolower(str_replace(['_', '-'], [' ', ' '], $string)))); + } + + /** + * Convert any string (including php headers with HTTP prefix) to header format like : + * * X-Pingother -> HTTP_X_PINGOTHER + * * X PINGOTHER -> HTTP_X_PINGOTHER + * @param string $string string to convert + * @return string the result in "php $_SERVER header" format + */ + protected static function headerizeToPhp($string) + { + return 'HTTP_'.strtoupper(str_replace([' ', '-'], ['_', '_'], $string)); + } + + /** + * For each CORS headers create the specific response + * @param array $requestHeaders CORS headers we have detected + * @return array CORS headers ready to be sent + */ + public function prepareHeaders($requestHeaders) + { + $responseHeaders = []; + // handle Origin + if (isset($requestHeaders['Origin'])) { + if ((in_array('*', $this->cors['Origin']) === true) + || (in_array($requestHeaders['Origin'], $this->cors['Origin']))) { + $responseHeaders['Access-Control-Allow-Origin'] = $requestHeaders['Origin']; + } + } + $this->prepareAllowHeaders('Method', $requestHeaders, $responseHeaders); + $this->prepareAllowHeaders('Headers', $requestHeaders, $responseHeaders); + if ($this->cors['Access-Control-Allow-Credentials'] === true) { + $responseHeaders['Access-Control-Allow-Credentials'] = 'true'; + } elseif ($this->cors['Access-Control-Allow-Credentials'] === false) { + $responseHeaders['Access-Control-Allow-Credentials'] = 'false'; + } + if (($_SERVER['REQUEST_METHOD'] === 'OPTIONS') && ($this->cors['Access-Control-Max-Age'] !== null)) { + $responseHeaders['Access-Control-Max-Age'] = $this->cors['Access-Control-Max-Age']; + } + + return $responseHeaders; + } + + /** + * Adds the CORS headers to the response + * @param Response $response + * @param array CORS headers which have been compouted + */ + public function addCorsHeaders($response, $headers) + { + if (empty($headers) === false) { + $responseHeaders = $response->getHeaders(); + foreach ($headers as $field => $value) { + $responseHeaders->set($field, $value); + } + } + } +}