From c03a3ff858ce90a6f5df1cdf7994185f33f661e2 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 27 Mar 2013 11:39:05 -0400 Subject: [PATCH] Finishes flash feature. --- framework/base/Dictionary.php | 8 +- framework/base/Vector.php | 6 +- framework/web/Session.php | 157 ++++++++++++++++++++---- framework/web/User.php | 174 +++++---------------------- tests/unit/framework/base/DictionaryTest.php | 24 ++-- tests/unit/framework/base/VectorTest.php | 14 +-- 6 files changed, 186 insertions(+), 197 deletions(-) diff --git a/framework/base/Dictionary.php b/framework/base/Dictionary.php index 9343d68..52262cb 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -148,7 +148,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * Defaults to false, meaning all items in the dictionary will be cleared directly * without calling [[remove]]. */ - public function clear($safeClear = false) + public function removeAll($safeClear = false) { if ($safeClear) { foreach (array_keys($this->_d) as $key) { @@ -164,7 +164,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * @param mixed $key the key * @return boolean whether the dictionary contains an item with the specified key */ - public function contains($key) + public function has($key) { return isset($this->_d[$key]) || array_key_exists($key, $this->_d); } @@ -188,7 +188,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co { if (is_array($data) || $data instanceof \Traversable) { if ($this->_d !== array()) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; @@ -252,7 +252,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co */ public function offsetExists($offset) { - return $this->contains($offset); + return $this->has($offset); } /** diff --git a/framework/base/Vector.php b/framework/base/Vector.php index 18f7037..7d43fdb 100644 --- a/framework/base/Vector.php +++ b/framework/base/Vector.php @@ -191,7 +191,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Defaults to false, meaning all items in the vector will be cleared directly * without calling [[removeAt]]. */ - public function clear($safeClear = false) + public function removeAll($safeClear = false) { if ($safeClear) { for ($i = $this->_c - 1; $i >= 0; --$i) { @@ -209,7 +209,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * @param mixed $item the item * @return boolean whether the vector contains the item */ - public function contains($item) + public function has($item) { return $this->indexOf($item) >= 0; } @@ -246,7 +246,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta { if (is_array($data) || $data instanceof \Traversable) { if ($this->_c > 0) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; diff --git a/framework/web/Session.php b/framework/web/Session.php index 5697679..eefc1a8 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -12,13 +12,15 @@ use yii\base\Component; use yii\base\InvalidParamException; /** - * Session provides session-level data management and the related configurations. + * Session provides session data management and the related configurations. * + * Session is a Web application component that can be accessed via `Yii::$app->session`. + * To start the session, call [[open()]]; To complete and send out session data, call [[close()]]; * To destroy the session, call [[destroy()]]. * - * If [[autoStart]] is set true, the session will be started automatically - * when the application component is initialized by the application. + * By default, [[autoStart]] is true which means the session will be started automatically + * when the session component is accessed the first time. * * Session can be used like an array to set and get session data. For example, * @@ -37,22 +39,11 @@ use yii\base\InvalidParamException; * [[openSession()]], [[closeSession()]], [[readSession()]], [[writeSession()]], * [[destroySession()]] and [[gcSession()]]. * - * Session is a Web application component that can be accessed via - * `Yii::$app->session`. - * - * @property boolean $useCustomStorage read-only. Whether to use custom storage. - * @property boolean $isActive Whether the session has started. - * @property string $id The current session ID. - * @property string $name The current session name. - * @property string $savePath The current session save path, defaults to '/tmp'. - * @property array $cookieParams The session cookie parameters. - * @property string $cookieMode How to use cookie to store session ID. Defaults to 'Allow'. - * @property float $gcProbability The probability (percentage) that the gc (garbage collection) process is started on every session initialization. - * @property boolean $useTransparentSessionID Whether transparent sid support is enabled or not, defaults to false. - * @property integer $timeout The number of seconds after which data will be seen as 'garbage' and cleaned up, defaults to 1440 seconds. - * @property SessionIterator $iterator An iterator for traversing the session variables. - * @property integer $count The number of session variables. - * @property array $keys The list of session variable names. + * Session also supports a special type of session data, called *flash messages*. + * A flash message is available only in the current request and the next request. + * After that, it will be deleted automatically. Flash messages are particularly + * useful for displaying confirmation messages. To use flash messages, simply + * call methods such as [[setFlash()]], [[getFlash()]]. * * @author Qiang Xue * @since 2.0 @@ -63,6 +54,10 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * @var boolean whether the session should be automatically started when the session component is initialized. */ public $autoStart = true; + /** + * @var string the name of the session variable that stores the flash message data. + */ + public $flashVar = '__flash'; /** * Initializes the application component. @@ -117,7 +112,9 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co if (session_id() == '') { $error = error_get_last(); $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::warning($message, __CLASS__); + Yii::error($message, __CLASS__); + } else { + $this->updateFlashCounters(); } } @@ -462,18 +459,18 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co /** * Adds a session variable. - * Note, if the specified name already exists, the old value will be removed first. - * @param mixed $key session variable name + * If the specified name already exists, the old value will be overwritten. + * @param string $key session variable name * @param mixed $value session variable value */ - public function add($key, $value) + public function set($key, $value) { $_SESSION[$key] = $value; } /** * Removes a session variable. - * @param mixed $key the name of the session variable to be removed + * @param string $key the name of the session variable to be removed * @return mixed the removed value, null if no such session variable. */ public function remove($key) @@ -490,7 +487,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co /** * Removes all session variables */ - public function clear() + public function removeAll() { foreach (array_keys($_SESSION) as $key) { unset($_SESSION[$key]); @@ -501,7 +498,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * @param mixed $key session variable name * @return boolean whether there is the named session variable */ - public function contains($key) + public function has($key) { return isset($_SESSION[$key]); } @@ -515,6 +512,114 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co } /** + * Updates the counters for flash messages and removes outdated flash messages. + * This method should only be called once in [[init()]]. + */ + protected function updateFlashCounters() + { + $counters = $this->get($this->flashVar, array()); + if (is_array($counters)) { + foreach ($counters as $key => $count) { + if ($count) { + unset($counters[$key], $_SESSION[$key]); + } else { + $counters[$key]++; + } + } + $_SESSION[$this->flashVar] = $counters; + } else { + // fix the unexpected problem that flashVar doesn't return an array + unset($_SESSION[$this->flashVar]); + } + } + + /** + * Returns a flash message. + * A flash message is available only in the current request and the next request. + * @param string $key the key identifying the flash message + * @param mixed $defaultValue value to be returned if the flash message does not exist. + * @return mixed the flash message + */ + public function getFlash($key, $defaultValue = null) + { + $counters = $this->get($this->flashVar, array()); + return isset($counters[$key]) ? $this->get($key, $defaultValue) : $defaultValue; + } + + /** + * Returns all flash messages. + * @return array flash messages (key => message). + */ + public function getAllFlashes() + { + $counters = $this->get($this->flashVar, array()); + $flashes = array(); + foreach (array_keys($counters) as $key) { + if (isset($_SESSION[$key])) { + $flashes[$key] = $_SESSION[$key]; + } + } + return $flashes; + } + + /** + * Stores a flash message. + * A flash message is available only in the current request and the next request. + * @param string $key the key identifying the flash message. Note that flash messages + * and normal session variables share the same name space. If you have a normal + * session variable using the same name, its value will be overwritten by this method. + * @param mixed $value flash message + */ + public function setFlash($key, $value) + { + $counters = $this->get($this->flashVar, array()); + $counters[$key] = 0; + $_SESSION[$key] = $value; + $_SESSION[$this->flashVar] = $counters; + } + + /** + * Removes a flash message. + * Note that flash messages will be automatically removed after the next request. + * @param string $key the key identifying the flash message. Note that flash messages + * and normal session variables share the same name space. If you have a normal + * session variable using the same name, it will be removed by this method. + * @return mixed the removed flash message. Null if the flash message does not exist. + */ + public function removeFlash($key) + { + $counters = $this->get($this->flashVar, array()); + $value = isset($_SESSION[$key], $counters[$key]) ? $_SESSION[$key] : null; + unset($counters[$key], $_SESSION[$key]); + $_SESSION[$this->flashVar] = $counters; + return $value; + } + + /** + * Removes all flash messages. + * Note that flash messages and normal session variables share the same name space. + * If you have a normal session variable using the same name, it will be removed + * by this method. + */ + public function removeAllFlashes() + { + $counters = $this->get($this->flashVar, array()); + foreach (array_keys($counters) as $key) { + unset($_SESSION[$key]); + } + unset($_SESSION[$this->flashVar]); + } + + /** + * @param string $key key identifying the flash message + * @return boolean whether the specified flash message exists + */ + public function hasFlash($key) + { + return $this->getFlash($key) !== null; + } + + /** * This method is required by the interface ArrayAccess. * @param mixed $offset the offset to check on * @return boolean diff --git a/framework/web/User.php b/framework/web/User.php index 4bd184f..93eb1ce 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -40,7 +40,7 @@ use yii\base\Component; * Both {@link id} and {@link name} are persistent during the user session. * Besides, an identity may have additional persistent data which can * be accessed by calling {@link getState}. - * Note, when {@link allowAutoLogin cookie-based authentication} is enabled, + * Note, when {@link enableAutoLogin cookie-based authentication} is enabled, * all these persistent data will be stored in cookie. Therefore, do not * store password or other sensitive data in the persistent storage. Instead, * you should store them directly in session on the server side if needed. @@ -50,67 +50,54 @@ use yii\base\Component; * @property string $name The user name. If the user is not logged in, this will be {@link guestName}. * @property string $returnUrl The URL that the user should be redirected to after login. * @property string $stateKeyPrefix A prefix for the name of the session variables storing user session data. - * @property array $flashes Flash messages (key => message). * * @author Qiang Xue * @since 2.0 */ class User extends Component { - const FLASH_KEY_PREFIX = 'Yii.CWebUser.flash.'; - const FLASH_COUNTERS = 'Yii.CWebUser.flashcounters'; const STATES_VAR = '__states'; const AUTH_TIMEOUT_VAR = '__timeout'; /** * @var boolean whether to enable cookie-based login. Defaults to false. */ - public $allowAutoLogin = false; + public $enableAutoLogin = false; /** - * @var string the name for a guest user. Defaults to 'Guest'. - * This is used by {@link getName} when the current user is a guest (not authenticated). - */ - public $guestName = 'Guest'; - /** - * @var string|array the URL for login. If using array, the first element should be - * the route to the login action, and the rest name-value pairs are GET parameters - * to construct the login URL (e.g. array('/site/login')). If this property is null, - * a 403 HTTP exception will be raised instead. - * @see CController::createUrl + * @var string|array the URL for login when [[loginRequired()]] is called. + * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. + * The first element of the array should be the route to the login action, and the rest of + * the name-value pairs are GET parameters used to construct the login URL. For example, + * + * ~~~ + * array('site/login', 'ref' => 1) + * ~~~ + * + * If this property is null, a 403 HTTP exception will be raised when [[loginRequired()]] is called. */ - public $loginUrl = array('/site/login'); + public $loginUrl = array('site/login'); /** - * @var array the property values (in name-value pairs) used to initialize the identity cookie. - * Any property of {@link CHttpCookie} may be initialized. - * This property is effective only when {@link allowAutoLogin} is true. + * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. + * @see Cookie */ public $identityCookie; /** - * @var integer timeout in seconds after which user is logged out if inactive. - * If this property is not set, the user will be logged out after the current session expires - * (c.f. {@link CHttpSession::timeout}). - * @since 1.1.7 + * @var integer the number of seconds in which the user will be logged out automatically if he + * remains inactive. If this property is not set, the user will be logged out after + * the current session expires (c.f. [[Session::timeout]]). */ public $authTimeout; /** * @var boolean whether to automatically renew the identity cookie each time a page is requested. - * Defaults to false. This property is effective only when {@link allowAutoLogin} is true. + * Defaults to false. This property is effective only when {@link enableAutoLogin} is true. * When this is false, the identity cookie will expire after the specified duration since the user * is initially logged in. When this is true, the identity cookie will expire after the specified duration * since the user visits the site the last time. - * @see allowAutoLogin + * @see enableAutoLogin * @since 1.1.0 */ public $autoRenewCookie = false; /** - * @var boolean whether to automatically update the validity of flash messages. - * Defaults to true, meaning flash messages will be valid only in the current and the next requests. - * If this is set false, you will be responsible for ensuring a flash message is deleted after usage. - * (This can be achieved by calling {@link getFlash} with the 3rd parameter being true). - * @since 1.1.7 - */ - public $autoUpdateFlash = true; - /** * @var string value that will be echoed in case that user session has expired during an ajax call. * When a request is made and user session has expired, {@link loginRequired} redirects to {@link loginUrl} for login. * If that happens during an ajax call, the complete HTML login page is returned as the result of that ajax call. That could be @@ -129,22 +116,16 @@ class User extends Component /** * Initializes the application component. - * This method overrides the parent implementation by starting session, - * performing cookie-based authentication if enabled, and updating the flash variables. */ public function init() { parent::init(); Yii::app()->getSession()->open(); - if ($this->getIsGuest() && $this->allowAutoLogin) { + if ($this->getIsGuest() && $this->enableAutoLogin) { $this->restoreFromCookie(); - } elseif ($this->autoRenewCookie && $this->allowAutoLogin) { + } elseif ($this->autoRenewCookie && $this->enableAutoLogin) { $this->renewCookie(); } - if ($this->autoUpdateFlash) { - $this->updateFlash(); - } - $this->updateAuthStatus(); } @@ -156,12 +137,12 @@ class User extends Component * the session storage. If the duration parameter is greater than 0, * a cookie will be sent to prepare for cookie-based login in future. * - * Note, you have to set {@link allowAutoLogin} to true + * Note, you have to set {@link enableAutoLogin} to true * if you want to allow user to be authenticated based on the cookie information. * * @param IUserIdentity $identity the user identity (which should already be authenticated) * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. - * If greater than 0, cookie-based login will be used. In this case, {@link allowAutoLogin} + * If greater than 0, cookie-based login will be used. In this case, {@link enableAutoLogin} * must be set true, otherwise an exception will be thrown. * @return boolean whether the user is logged in */ @@ -173,10 +154,10 @@ class User extends Component $this->changeIdentity($id, $identity->getName(), $states); if ($duration > 0) { - if ($this->allowAutoLogin) { + if ($this->enableAutoLogin) { $this->saveToCookie($duration); } else { - throw new CException(Yii::t('yii', '{class}.allowAutoLogin must be set true in order to use cookie-based authentication.', + throw new CException(Yii::t('yii', '{class}.enableAutoLogin must be set true in order to use cookie-based authentication.', array('{class}' => get_class($this)))); } } @@ -196,7 +177,7 @@ class User extends Component public function logout($destroySession = true) { if ($this->beforeLogout()) { - if ($this->allowAutoLogin) { + if ($this->enableAutoLogin) { Yii::app()->getRequest()->getCookies()->remove($this->getStateKeyPrefix()); if ($this->identityCookie !== null) { $cookie = $this->createIdentityCookie($this->getStateKeyPrefix()); @@ -377,7 +358,7 @@ class User extends Component /** * Populates the current user object with the information obtained from cookie. - * This method is used when automatic login ({@link allowAutoLogin}) is enabled. + * This method is used when automatic login ({@link enableAutoLogin}) is enabled. * The user identity information is recovered from cookie. * Sufficient security measures are used to prevent cookie data from being tampered. * @see saveToCookie @@ -425,7 +406,7 @@ class User extends Component /** * Saves necessary user data into a cookie. - * This method is used when automatic login ({@link allowAutoLogin}) is enabled. + * This method is used when automatic login ({@link enableAutoLogin}) is enabled. * This method saves user ID, username, other identity states and a validation key to cookie. * These information are used to do authentication next time when user visits the application. * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. @@ -555,81 +536,6 @@ class User extends Component } /** - * Returns all flash messages. - * This method is similar to {@link getFlash} except that it returns all - * currently available flash messages. - * @param boolean $delete whether to delete the flash messages after calling this method. - * @return array flash messages (key => message). - * @since 1.1.3 - */ - public function getFlashes($delete = true) - { - $flashes = array(); - $prefix = $this->getStateKeyPrefix() . self::FLASH_KEY_PREFIX; - $keys = array_keys($_SESSION); - $n = strlen($prefix); - foreach ($keys as $key) { - if (!strncmp($key, $prefix, $n)) { - $flashes[substr($key, $n)] = $_SESSION[$key]; - if ($delete) { - unset($_SESSION[$key]); - } - } - } - if ($delete) { - $this->setState(self::FLASH_COUNTERS, array()); - } - return $flashes; - } - - /** - * Returns a flash message. - * A flash message is available only in the current and the next requests. - * @param string $key key identifying the flash message - * @param mixed $defaultValue value to be returned if the flash message is not available. - * @param boolean $delete whether to delete this flash message after accessing it. - * Defaults to true. - * @return mixed the message message - */ - public function getFlash($key, $defaultValue = null, $delete = true) - { - $value = $this->getState(self::FLASH_KEY_PREFIX . $key, $defaultValue); - if ($delete) { - $this->setFlash($key, null); - } - return $value; - } - - /** - * Stores a flash message. - * A flash message is available only in the current and the next requests. - * @param string $key key identifying the flash message - * @param mixed $value flash message - * @param mixed $defaultValue if this value is the same as the flash message, the flash message - * will be removed. (Therefore, you can use setFlash('key',null) to remove a flash message.) - */ - public function setFlash($key, $value, $defaultValue = null) - { - $this->setState(self::FLASH_KEY_PREFIX . $key, $value, $defaultValue); - $counters = $this->getState(self::FLASH_COUNTERS, array()); - if ($value === $defaultValue) { - unset($counters[$key]); - } else { - $counters[$key] = 0; - } - $this->setState(self::FLASH_COUNTERS, $counters, array()); - } - - /** - * @param string $key key identifying the flash message - * @return boolean whether the specified flash message exists - */ - public function hasFlash($key) - { - return $this->getFlash($key, null, false) !== null; - } - - /** * Changes the current user with the specified identity information. * This method is called by {@link login} and {@link restoreFromCookie} * when the current user needs to be populated with the corresponding @@ -678,28 +584,6 @@ class User extends Component } /** - * Updates the internal counters for flash messages. - * This method is internally used by {@link CWebApplication} - * to maintain the availability of flash messages. - */ - protected function updateFlash() - { - $counters = $this->getState(self::FLASH_COUNTERS); - if (!is_array($counters)) { - return; - } - foreach ($counters as $key => $count) { - if ($count) { - unset($counters[$key]); - $this->setState(self::FLASH_KEY_PREFIX . $key, null); - } else { - $counters[$key]++; - } - } - $this->setState(self::FLASH_COUNTERS, $counters, array()); - } - - /** * Updates the authentication status according to {@link authTimeout}. * If the user has been inactive for {@link authTimeout} seconds, * he will be automatically logged out. diff --git a/tests/unit/framework/base/DictionaryTest.php b/tests/unit/framework/base/DictionaryTest.php index 0b20093..9e55547 100644 --- a/tests/unit/framework/base/DictionaryTest.php +++ b/tests/unit/framework/base/DictionaryTest.php @@ -61,7 +61,7 @@ class DictionaryTest extends \yiiunit\TestCase { $this->dictionary->add('key3',$this->item3); $this->assertEquals(3,$this->dictionary->getCount()); - $this->assertTrue($this->dictionary->contains('key3')); + $this->assertTrue($this->dictionary->has('key3')); $this->dictionary[] = 'test'; } @@ -70,28 +70,28 @@ class DictionaryTest extends \yiiunit\TestCase { $this->dictionary->remove('key1'); $this->assertEquals(1,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key1')); + $this->assertTrue(!$this->dictionary->has('key1')); $this->assertTrue($this->dictionary->remove('unknown key')===null); } - public function testClear() + public function testRemoveAll() { $this->dictionary->add('key3',$this->item3); - $this->dictionary->clear(); + $this->dictionary->removeAll(); $this->assertEquals(0,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key1') && !$this->dictionary->contains('key2')); + $this->assertTrue(!$this->dictionary->has('key1') && !$this->dictionary->has('key2')); $this->dictionary->add('key3',$this->item3); - $this->dictionary->clear(true); + $this->dictionary->removeAll(true); $this->assertEquals(0,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key1') && !$this->dictionary->contains('key2')); + $this->assertTrue(!$this->dictionary->has('key1') && !$this->dictionary->has('key2')); } - public function testContains() + public function testHas() { - $this->assertTrue($this->dictionary->contains('key1')); - $this->assertTrue($this->dictionary->contains('key2')); - $this->assertFalse($this->dictionary->contains('key3')); + $this->assertTrue($this->dictionary->has('key1')); + $this->assertTrue($this->dictionary->has('key2')); + $this->assertFalse($this->dictionary->has('key3')); } public function testFromArray() @@ -162,7 +162,7 @@ class DictionaryTest extends \yiiunit\TestCase unset($this->dictionary['key2']); $this->assertEquals(2,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key2')); + $this->assertTrue(!$this->dictionary->has('key2')); unset($this->dictionary['unknown key']); } diff --git a/tests/unit/framework/base/VectorTest.php b/tests/unit/framework/base/VectorTest.php index d2657bf..5c44d17 100644 --- a/tests/unit/framework/base/VectorTest.php +++ b/tests/unit/framework/base/VectorTest.php @@ -101,26 +101,26 @@ class VectorTest extends \yiiunit\TestCase $this->vector->removeAt(2); } - public function testClear() + public function testRemoveAll() { $this->vector->add($this->item3); - $this->vector->clear(); + $this->vector->removeAll(); $this->assertEquals(0,$this->vector->getCount()); $this->assertEquals(-1,$this->vector->indexOf($this->item1)); $this->assertEquals(-1,$this->vector->indexOf($this->item2)); $this->vector->add($this->item3); - $this->vector->clear(true); + $this->vector->removeAll(true); $this->assertEquals(0,$this->vector->getCount()); $this->assertEquals(-1,$this->vector->indexOf($this->item1)); $this->assertEquals(-1,$this->vector->indexOf($this->item2)); } - public function testContains() + public function testHas() { - $this->assertTrue($this->vector->contains($this->item1)); - $this->assertTrue($this->vector->contains($this->item2)); - $this->assertFalse($this->vector->contains($this->item3)); + $this->assertTrue($this->vector->has($this->item1)); + $this->assertTrue($this->vector->has($this->item2)); + $this->assertFalse($this->vector->has($this->item3)); } public function testIndexOf()