From 1184e1e148b1f4fe1af61774e58581a3ffc2cba3 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 26 Mar 2013 15:27:15 -0400 Subject: [PATCH] Finished AccessControl and HttpCache. --- framework/web/AccessControl.php | 104 ++++++++++++++++++++ framework/web/AccessRule.php | 212 ++++++++++++++++++++++++++++++++++++++++ framework/web/Application.php | 12 +++ framework/web/HttpCache.php | 125 +++++++++++++++++++++++ framework/web/Request.php | 2 +- 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 framework/web/AccessControl.php create mode 100644 framework/web/AccessRule.php create mode 100644 framework/web/HttpCache.php diff --git a/framework/web/AccessControl.php b/framework/web/AccessControl.php new file mode 100644 index 0000000..793fb05 --- /dev/null +++ b/framework/web/AccessControl.php @@ -0,0 +1,104 @@ + + * @since 2.0 + */ +class AccessControl extends ActionFilter +{ + /** + * @var callback a callback that will be called if the access should be denied + * to the current user. If not set, [[denyAccess()]] will be called. + * + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + */ + public $denyCallback; + /** + * @var string the default class of the access rules. This is used when + * a rule is configured without specifying a class in [[rules]]. + */ + public $defaultRuleClass = 'yii\web\AccessRule'; + /** + * @var array a list of access rule objects or configurations for creating the rule objects. + */ + public $rules = array(); + + /** + * Initializes the [[rules]] array by instantiating rule objects from configurations. + */ + public function init() + { + parent::init(); + foreach ($this->rules as $i => $rule) { + if (is_array($rule)) { + if (!isset($rule['class'])) { + $rule['class'] = $this->defaultRuleClass; + } + $this->rules[$i] = Yii::createObject($rule); + } + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $user = Yii::$app->getUser(); + $request = Yii::$app->getRequest(); + /** @var $rule AccessRule */ + foreach ($this->rules as $rule) { + if ($allow = $rule->allows($action, $user, $request)) { + break; + } elseif ($allow === false) { + if (isset($rule->denyCallback)) { + call_user_func($rule->denyCallback, $rule); + } elseif (isset($this->denyCallback)) { + call_user_func($this->denyCallback, $rule); + } else { + $this->denyAccess($user); + } + return false; + } + } + return true; + } + + /** + * Denies the access of the user. + * The default implementation will redirect the user to the login page if he is a guest; + * if the user is already logged, a 403 HTTP exception will be thrown. + * @param User $user the current user + * @throws HttpException if the user is already logged in. + */ + protected function denyAccess($user) + { + if ($user->getIsGuest()) { + $user->loginRequired(); + } else { + throw new HttpException(403, Yii::t('yii|You are not allowed to perform this action.')); + } + } +} \ No newline at end of file diff --git a/framework/web/AccessRule.php b/framework/web/AccessRule.php new file mode 100644 index 0000000..ac42ad1 --- /dev/null +++ b/framework/web/AccessRule.php @@ -0,0 +1,212 @@ + + * @since 2.0 + */ +class AccessRule extends Component +{ + /** + * @var boolean whether this is an 'allow' rule or 'deny' rule. + */ + public $allow; + /** + * @var array list of action IDs that this rule applies to. The comparison is case-sensitive. + * If not set or empty, it means this rule applies to all actions. + */ + public $actions; + /** + * @var array list of controller IDs that this rule applies to. The comparison is case-sensitive. + * If not set or empty, it means this rule applies to all controllers. + */ + public $controllers; + /** + * @var array list of user names that this rule applies to. The comparison is case-insensitive. + * If not set or empty, it means this rule applies to all users. Two special tokens are recognized: + * + * - `?`: matches a guest user (not authenticated yet) + * - `@`: matches an authenticated user + * + * @see \yii\web\Application::user + */ + public $users; + /** + * @var array list of roles that this rule applies to. For each role, the current user's + * {@link CWebUser::checkAccess} method will be invoked. If one of the invocations + * returns true, the rule will be applied. + * Note, you should mainly use roles in an "allow" rule because by definition, + * a role represents a permission collection. + * If not set or empty, it means this rule applies to all roles. + */ + public $roles; + /** + * @var array list of user IP addresses that this rule applies to. An IP address + * can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix. + * For example, '192.168.*' matches all IP addresses in the segment '192.168.'. + * If not set or empty, it means this rule applies to all IP addresses. + * @see Request::userIP + */ + public $ips; + /** + * @var array list of request methods (e.g. `GET`, `POST`) that this rule applies to. + * The request methods must be specified in uppercase. + * If not set or empty, it means this rule applies to all request methods. + * @see Request::requestMethod + */ + public $verbs; + /** + * @var callback a callback that will be called to determine if the rule should be applied. + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + * The callback should return a boolean value indicating whether this rule should be applied. + */ + public $matchCallback; + /** + * @var callback a callback that will be called if this rule determines the access to + * the current action should be denied. If not set, the behavior will be determined by + * [[AccessControl]]. + * + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + */ + public $denyCallback; + + + /** + * Checks whether the Web user is allowed to perform the specified action. + * @param Action $action the action to be performed + * @param User $user the user object + * @param Request $request + * @return boolean|null true if the user is allowed, false if the user is denied, null if the rule does not apply to the user + */ + public function allows($action, $user, $request) + { + if ($this->matchAction($action) + && $this->matchUser($user) + && $this->matchRole($user) + && $this->matchIP($request->getUserIP()) + && $this->matchVerb($request->getRequestMethod()) + && $this->matchController($action->controller) + && $this->matchCustom($action) + ) { + return $this->allow ? true : false; + } else { + return null; + } + } + + /** + * @param Action $action the action + * @return boolean whether the rule applies to the action + */ + protected function matchAction($action) + { + return empty($this->actions) || in_array($action->id, $this->actions, true); + } + + /** + * @param Controller $controller the controller + * @return boolean whether the rule applies to the controller + */ + protected function matchController($controller) + { + return empty($this->controllers) || in_array($controller->id, $this->controllers, true); + } + + /** + * @param User $user the user + * @return boolean whether the rule applies to the user + */ + protected function matchUser($user) + { + if (empty($this->users)) { + return true; + } + foreach ($this->users as $u) { + if ($u === '?' && $user->getIsGuest()) { + return true; + } elseif ($u === '@' && !$user->getIsGuest()) { + return true; + } elseif (!strcasecmp($u, $user->getName())) { + return true; + } + } + return false; + } + + /** + * @param User $user the user object + * @return boolean whether the rule applies to the role + */ + protected function matchRole($user) + { + if (empty($this->roles)) { + return true; + } + foreach ($this->roles as $role) { + if ($user->checkAccess($role)) { + return true; + } + } + return false; + } + + /** + * @param string $ip the IP address + * @return boolean whether the rule applies to the IP address + */ + protected function matchIP($ip) + { + if (empty($this->ips)) { + return true; + } + foreach ($this->ips as $rule) { + if ($rule === '*' || $rule === $ip || (($pos = strpos($rule, '*')) !== false && !strncmp($ip, $rule, $pos))) { + return true; + } + } + return false; + } + + /** + * @param string $verb the request method + * @return boolean whether the rule applies to the request + */ + protected function matchVerb($verb) + { + return empty($this->verbs) || in_array($verb, $this->verbs, true); + } + + /** + * @param Action $action the action to be performed + * @return boolean whether the rule should be applied + */ + protected function matchCustom($action) + { + return empty($this->matchCallback) || call_user_func($this->matchCallback, $this, $action); + } +} \ No newline at end of file diff --git a/framework/web/Application.php b/framework/web/Application.php index 6e0cc73..2e3a0c3 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -69,6 +69,15 @@ class Application extends \yii\base\Application } /** + * Returns the user component. + * @return User the user component + */ + public function getUser() + { + return $this->getComponent('user'); + } + + /** * Creates a URL using the given route and parameters. * * This method first normalizes the given route by converting a relative route into an absolute one. @@ -130,6 +139,9 @@ class Application extends \yii\base\Application 'session' => array( 'class' => 'yii\web\Session', ), + 'user' => array( + 'class' => 'yii\web\User', + ), )); } } diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php new file mode 100644 index 0000000..e3cf17d --- /dev/null +++ b/framework/web/HttpCache.php @@ -0,0 +1,125 @@ + + * @author Qiang Xue + * @since 2.0 + */ +class HttpCache extends ActionFilter +{ + /** + * @var callback a PHP callback that returns the UNIX timestamp of the last modification time. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. + */ + public $lastModified; + /** + * @var callback a PHP callback that generates the Etag seed string. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. The callback should return a string. + */ + public $etagSeed; + /** + * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. + */ + public $params; + /** + * Http cache control headers. Set this to an empty string in order to keep this + * header from being sent entirely. + * @var string + */ + public $cacheControl = 'max-age=3600, public'; + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $requestMethod = Yii::$app->request->getRequestMethod(); + if ($requestMethod !== 'GET' && $requestMethod !== 'HEAD') { + return true; + } + + $lastModified = $etag = null; + if ($this->lastModified !== null) { + $lastModified = call_user_func($this->lastModified, $action, $this->params); + } + if ($this->etagSeed !== null) { + $seed = call_user_func($this->etagSeed, $action, $this->params); + $etag = $this->generateEtag($seed); + } + + if ($etag !== null) { + header("ETag: $etag"); + } + $this->sendCacheControlHeader(); + + if ($this->hasChanged($lastModified, $etag)) { + if ($lastModified !== null) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + } + return true; + } else { + header('HTTP/1.1 304 Not Modified'); + return false; + } + } + + protected function hasChanged($lastModified, $etag) + { + $changed = !isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified; + if (!$changed && $etag !== null) { + $changed = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] !== $etag; + } + return $changed; + } + + /** + * Sends the cache control header to the client + * @see cacheControl + */ + protected function sendCacheControlHeader() + { + if (Yii::$app->session->isActive) { + session_cache_limiter('public'); + header('Pragma:', true); + } + header('Cache-Control: ' . $this->cacheControl, true); + } + + /** + * Generates an Etag from a given seed string. + * @param string $seed Seed for the ETag + * @return string the generated Etag + */ + protected function generateEtag($seed) + { + return '"' . base64_encode(sha1($seed, true)) . '"'; + } +} \ No newline at end of file diff --git a/framework/web/Request.php b/framework/web/Request.php index c7899cf..093a394 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -530,7 +530,7 @@ class Request extends \yii\base\Request * Returns the user IP address. * @return string user IP address */ - public function getUserHostAddress() + public function getUserIP() { return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; }