Controller.php 6.84 KB
Newer Older
Qiang Xue committed
1 2 3 4 5 6 7 8 9 10
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\rest;

use Yii;
Qiang Xue committed
11
use yii\base\InvalidConfigException;
Qiang Xue committed
12 13
use yii\web\Response;
use yii\web\UnauthorizedHttpException;
Qiang Xue committed
14
use yii\web\UnsupportedMediaTypeHttpException;
15
use yii\web\TooManyRequestsHttpException;
Qiang Xue committed
16 17 18 19 20 21 22 23
use yii\web\VerbFilter;

/**
 * Controller is the base class for RESTful API controller classes.
 *
 * Controller implements the following steps in a RESTful API request handling cycle:
 *
 * 1. Resolving response format and API version number (see [[supportedFormats]], [[supportedVersions]] and [[version]]);
Qiang Xue committed
24
 * 2. Validating request method (see [[verbs()]]).
Qiang Xue committed
25 26 27 28 29 30 31 32 33
 * 3. Authenticating user (see [[authenticate()]]);
 * 4. Formatting response data (see [[serializeData()]]).
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Controller extends \yii\web\Controller
{
	/**
Qiang Xue committed
34
	 * @var string the name of the header parameter representing the API version number.
Qiang Xue committed
35
	 */
Qiang Xue committed
36
	public $versionHeaderParam = 'version';
Qiang Xue committed
37 38 39 40 41 42 43 44
	/**
	 * @var string|array the configuration for creating the serializer that formats the response data.
	 */
	public $serializer = 'yii\rest\Serializer';
	/**
	 * @inheritdoc
	 */
	public $enableCsrfValidation = false;
45
	/**
Qiang Xue committed
46 47
	 * @var array the supported authentication methods. This property should take a list of supported
	 * authentication methods, each represented by an authentication class or configuration.
Qiang Xue committed
48
	 * If this is not set or empty, it means authentication is disabled.
49
	 */
Qiang Xue committed
50
	public $authMethods;
Qiang Xue committed
51 52
	/**
	 * @var string|array the rate limiter class or configuration. If this is not set or empty,
Qiang Xue committed
53 54
	 * the rate limiting will be disabled. Note that if the user is not authenticated, the rate limiting
	 * will also NOT be performed.
Qiang Xue committed
55
	 * @see checkRateLimit()
Qiang Xue committed
56
	 * @see authMethods
Qiang Xue committed
57 58
	 */
	public $rateLimiter = 'yii\rest\RateLimiter';
Qiang Xue committed
59
	/**
Qiang Xue committed
60
	 * @var string the chosen API version number, or null if [[supportedVersions]] is empty.
Qiang Xue committed
61 62 63 64 65
	 * @see supportedVersions
	 */
	public $version;
	/**
	 * @var array list of supported API version numbers. If the current request does not specify a version
Qiang Xue committed
66 67
	 * number, the first element will be used as the [[version|chosen version number]]. For this reason, you should
	 * put the latest version number at the first. If this property is empty, [[version]] will not be set.
Qiang Xue committed
68
	 */
Qiang Xue committed
69
	public $supportedVersions = [];
Qiang Xue committed
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
	/**
	 * @var array list of supported response formats. The array keys are the requested content MIME types,
	 * and the array values are the corresponding response formats. The first element will be used
	 * as the response format if the current request does not specify a content type.
	 */
	public $supportedFormats = [
		'application/json' => Response::FORMAT_JSON,
		'application/xml' => Response::FORMAT_XML,
	];

	/**
	 * @inheritdoc
	 */
	public function behaviors()
	{
		return [
			'verbFilter' => [
				'class' => VerbFilter::className(),
				'actions' => $this->verbs(),
			],
		];
	}

	/**
	 * @inheritdoc
	 */
	public function init()
	{
		parent::init();
		$this->resolveFormatAndVersion();
	}

	/**
	 * @inheritdoc
	 */
	public function beforeAction($action)
	{
		if (parent::beforeAction($action)) {
			$this->authenticate();
109
			$this->checkRateLimit($action);
Qiang Xue committed
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
			return true;
		} else {
			return false;
		}
	}

	/**
	 * @inheritdoc
	 */
	public function afterAction($action, $result)
	{
		$result = parent::afterAction($action, $result);
		return $this->serializeData($result);
	}

	/**
	 * Resolves the response format and the API version number.
Qiang Xue committed
127
	 * @throws UnsupportedMediaTypeHttpException
Qiang Xue committed
128 129 130
	 */
	protected function resolveFormatAndVersion()
	{
Qiang Xue committed
131
		$this->version = empty($this->supportedVersions) ? null : reset($this->supportedVersions);
Qiang Xue committed
132 133 134 135 136 137 138 139 140
		Yii::$app->getResponse()->format = reset($this->supportedFormats);
		$types = Yii::$app->getRequest()->getAcceptableContentTypes();
		if (empty($types)) {
			$types['*/*'] = [];
		}

		foreach ($types as $type => $params) {
			if (isset($this->supportedFormats[$type])) {
				Yii::$app->getResponse()->format = $this->supportedFormats[$type];
Qiang Xue committed
141 142 143
				if (isset($params[$this->versionHeaderParam])) {
					if (in_array($params[$this->versionHeaderParam], $this->supportedVersions, true)) {
						$this->version = $params[$this->versionHeaderParam];
Qiang Xue committed
144
					} else {
Qiang Xue committed
145
						throw new UnsupportedMediaTypeHttpException('You are requesting an invalid version number.');
Qiang Xue committed
146 147 148 149 150 151 152
					}
				}
				return;
			}
		}

		if (!isset($types['*/*'])) {
Qiang Xue committed
153
			throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.');
Qiang Xue committed
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
		}
	}

	/**
	 * Declares the allowed HTTP verbs.
	 * Please refer to [[VerbFilter::actions]] on how to declare the allowed verbs.
	 * @return array the allowed HTTP verbs.
	 */
	protected function verbs()
	{
		return [];
	}

	/**
	 * Authenticates the user.
169
	 * This method implements the user authentication based on an access token sent through the `Authorization` HTTP header.
Qiang Xue committed
170 171 172 173
	 * @throws UnauthorizedHttpException if the user is not authenticated successfully
	 */
	protected function authenticate()
	{
Qiang Xue committed
174 175
		if (empty($this->authMethods)) {
			return;
176 177
		}

Qiang Xue committed
178 179 180 181 182 183 184 185 186
		$user = Yii::$app->getUser();
		$request = Yii::$app->getRequest();
		$response = Yii::$app->getResponse();
		foreach ($this->authMethods as $i => $auth) {
			$this->authMethods[$i] = $auth = Yii::createObject($auth);
			if (!$auth instanceof AuthInterface) {
				throw new InvalidConfigException(get_class($auth) . ' must implement yii\rest\AuthInterface');
			} elseif ($auth->authenticate($user, $request, $response) !== null) {
				return;
187
			}
Qiang Xue committed
188
		}
Qiang Xue committed
189 190 191 192

		/** @var AuthInterface $auth */
		$auth = reset($this->authMethods);
		$auth->handleFailure($response);
Qiang Xue committed
193 194
	}

195 196
	/**
	 * Ensures the rate limit is not exceeded.
Qiang Xue committed
197 198 199 200 201
	 *
	 * This method will use [[rateLimiter]] to check rate limit. In order to perform rate limiting check,
	 * the user must be authenticated and the user identity object (`Yii::$app->user->identity`) must
	 * implement [[RateLimitInterface]].
	 *
202 203 204 205 206
	 * @param \yii\base\Action $action the action to be executed
	 * @throws TooManyRequestsHttpException if the rate limit is exceeded.
	 */
	protected function checkRateLimit($action)
	{
Qiang Xue committed
207 208 209 210 211 212 213 214 215 216
		if (empty($this->rateLimiter)) {
			return;
		}

		$identity = Yii::$app->getUser()->getIdentity(false);
		if ($identity instanceof RateLimitInterface) {
			/** @var RateLimiter $rateLimiter */
			$rateLimiter = Yii::createObject($this->rateLimiter);
			$rateLimiter->check($identity, Yii::$app->getRequest(), Yii::$app->getResponse(), $action);
		}
217 218
	}

Qiang Xue committed
219 220 221 222 223 224 225 226 227 228 229 230
	/**
	 * Serializes the specified data.
	 * The default implementation will create a serializer based on the configuration given by [[serializer]].
	 * It then uses the serializer to serialize the given data.
	 * @param mixed $data the data to be serialized
	 * @return mixed the serialized data.
	 */
	protected function serializeData($data)
	{
		return Yii::createObject($this->serializer)->serialize($data);
	}
}