Commit 1b5adca0 by Qiang Xue

Merge pull request #4193 from pgaultier/feature/CORS

CORS Filter
parents e6be9fd9 3b1d7fd0
......@@ -315,3 +315,86 @@ public function behaviors()
];
}
```
### [[yii\filters\Cors|Cors]] <a name="cors"></a>
Cross-origin resource sharing [CORS](https://developer.mozilla.org/fr/docs/HTTP/Access_control_CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.)
on a web page to be requested from another domain outside the domain the resource originated from.
In particular, JavaScript's AJAX calls can use the XMLHttpRequest mechanism. Such "cross-domain" requests would
otherwise be forbidden by web browsers, per the same origin security policy.
CORS defines a way in which the browser and the server can interact to determine whether or not to allow the cross-origin request.
The [[yii\filters\Cors|Cors filter]] should be defined before Authentication / Authorization filters to make sure the CORS headers
will always be sent.
```php
use yii\filters\Cors;
public function behaviors()
{
$behaviors = ArrayHelper::merge([
'corsHeaders' => [
'class' => Cors::className(),
],
], parent::behaviors());
return $behaviors;
}
```
The Cors filtering could be tuned using the `cors` property.
* `cors['Origin']`: array used to define allowed origins. Can be `['*']` (everyone) or `['http://www.myserver.net', 'http://www.myotherserver.com']`. Default to `['*']`.
* `cors['Access-Control-Request-Method']`: array of allowed verbs like `['GET', 'OPTIONS', 'HEAD']`. Default to `['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']`.
* `cors['Access-Control-Request-Headers']`: array of allowed headers. Can be `['*']` all headers or specific ones `['X-Request-With']`. Default to `['*']`.
* `cors['Access-Control-Allow-Credentials']`: define if current request can be made using credentials. Can be `true`, `false`. Default to `true`.
* `cors['Access-Control-Max-Age']`: define lifetime of pre-flight request. Default to `86400`.
For example, allowing CORS for origin : `http://www.myserver.net` with method `GET`, `HEAD` and `OPTIONS` and do not send `Access-Control-Allow-Credentials` header :
```php
use yii\filters\Cors;
public function behaviors()
{
$behaviors = ArrayHelper::merge([
'corsHeaders' => [
'class' => Cors::className(),
'cors' => [
'Origin' => ['http://www.myserver.net'],
'Access-Control-Request-Method' => ['GET', 'HEAD', 'OPTIONS'],
'Access-Control-Allow-Credentials' => null,
],
],
], parent::behaviors());
return $behaviors;
}
```
You may tune the CORS headers by overriding default parameters on a per action basis.
For example adding the `Access-Control-Allow-Credentials` for `login` action could be done like this :
```php
use yii\filters\Cors;
public function behaviors()
{
$behaviors = ArrayHelper::merge([
'corsHeaders' => [
'class' => Cors::className(),
'cors' => [
'Origin' => ['http://www.myserver.net'],
'Access-Control-Request-Method' => ['GET', 'HEAD', 'OPTIONS'],
'Access-Control-Allow-Credentials' => null,
],
'actions' => [
'login' => [
'Access-Control-Allow-Credentials' => true,
]
]
],
], parent::behaviors());
return $behaviors;
}
```
......@@ -188,6 +188,7 @@ Yii Framework 2 Change Log
- Chg: Added `prefix` column to `yii\log\DbTarget` to have the same amount of information logged as in files and emails (cebe)
- Chg: Use `limit(null)` instead of `limit(-1)` in migration controller to be compatible to more backends (cebe)
- New #3911: Added `yii\behaviors\SluggableBehavior` that fills the specified model attribute with the transliterated and adjusted version to use in URLs (creocoder)
- New #4193: Added `yii\filters\Cors` CORS filter to allow Cross Origin Resource Sharing (pgaultier)
2.0.0-beta April 13, 2014
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\filters;
use Yii;
use yii\base\ActionFilter;
use yii\web\Request;
use yii\web\Response;
/**
* Cors filter implements [Cross Origin Resource Sharing](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing).
* Make sure to read carefully what CORS does and does not. CORS do not secure your API,
* but allow the developper to grant access to third party code (ajax calls from external domain)
*
* You may use CORS filter by attaching it as a behavior to a controller or module, like the following,
*
* ```php
* public function behaviors()
* {
* return [
* 'corsFilter' => [
* '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 <pgaultier@gmail.com>
* @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 = $this->request ? : Yii::$app->getRequest();
$this->response = $this->response ? : Yii::$app->getResponse();
$this->overrideDefaultSettings($action);
$requestCorsHeaders = $this->extractHeaders();
$responseCorsHeaders = $this->prepareHeaders($requestCorsHeaders);
$this->addCorsHeaders($this->response, $responseCorsHeaders);
return true;
}
/**
* Override settings for specific action
* @param \yii\base\Action $action the action settings to override
*/
public function overrideDefaultSettings($action)
{
if (isset($this->actions[$action->id])) {
$actionParams = $this->actions[$action->id];
$actionParamsKeys = array_keys($actionParams);
foreach ($this->cors as $headerField => $headerValue) {
if (in_array($headerField, $actionParamsKeys)) {
$this->cors[$headerField] = $actionParams[$headerField];
}
}
}
}
/**
* Extract CORS headers fron the request
* @return array CORS headers to handle
*/
public function extractHeaders()
{
$headers = [];
$requestHeaders = array_keys($this->cors);
foreach ($requestHeaders as $headerField) {
$serverField = $this->headerizeToPhp($headerField);
$headerData = isset($_SERVER[$serverField])?$_SERVER[$serverField]:null;
if ($headerData !== null) {
$headers[$headerField] = $headerData;
}
}
return $headers;
}
/**
* 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;
}
/**
* 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($requestHeaders[$requestHeaderField]);
} elseif ($type === 'Headers') {
$responseHeaders[$responseHeaderField] = $this->headerize($requestHeaders[$requestHeaderField]);
}
} 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') {
$req = $this->headerize($req);
}
if (in_array($req, $this->cors[$requestHeaderField])) {
$acceptedData[] = $req;
}
}
if (empty($acceptedData) === false) {
$responseHeaders[$responseHeaderField] = implode(', ', $acceptedData);
}
}
}
}
/**
* 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);
}
}
}
/**
* Convert any string (including php headers with HTTP prefix) to header format like :
* * X-PINGOTHER -> X-Pingother
* * X_PINGOTHER -> X-Pingother
* @param string $string string to convert
* @return string the result in "header" format
*/
protected function headerize($string)
{
$headers = preg_split("/[\s,]+/", $string, -1, PREG_SPLIT_NO_EMPTY);
$headers = array_map(function($element) {
return str_replace(' ', '-', ucwords(strtolower(str_replace(['_', '-'], [' ', ' '], $element))));
}, $headers);
return implode(', ', $headers);
}
/**
* 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 function headerizeToPhp($string)
{
return 'HTTP_'.strtoupper(str_replace([' ', '-'], ['_', '_'], $string));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment